Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0458834681 | ||
|
|
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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,70 +259,69 @@ 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 newZoomValue = getNormalizedZoom(
|
||||||
|
clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom),
|
||||||
|
);
|
||||||
|
|
||||||
const centerScroll = centerScrollOn({
|
const centerScroll = centerScrollOn({
|
||||||
scenePoint: { x: centerX, y: centerY },
|
scenePoint: { x: centerX, y: centerY },
|
||||||
@@ -330,18 +329,15 @@ export const zoomToFitBounds = ({
|
|||||||
width: appState.width,
|
width: appState.width,
|
||||||
height: appState.height,
|
height: appState.height,
|
||||||
},
|
},
|
||||||
|
offsets: canvasOffsets,
|
||||||
zoom: { value: newZoomValue },
|
zoom: { value: newZoomValue },
|
||||||
});
|
});
|
||||||
|
|
||||||
scrollX = centerScroll.scrollX;
|
|
||||||
scrollY = centerScroll.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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 = <
|
||||||
|
|||||||
@@ -1,5 +1,90 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
import { clamp } from "../math/utils";
|
||||||
|
import tinycolor from "tinycolor2";
|
||||||
|
import { degreesToRadians } from "../math/angle";
|
||||||
|
import type { Degrees } from "../math/types";
|
||||||
|
|
||||||
|
function cssHueRotate(
|
||||||
|
red: number,
|
||||||
|
green: number,
|
||||||
|
blue: number,
|
||||||
|
degrees: Degrees,
|
||||||
|
): { r: number; g: number; b: number } {
|
||||||
|
// normalize
|
||||||
|
const r = red / 255;
|
||||||
|
const g = green / 255;
|
||||||
|
const b = blue / 255;
|
||||||
|
|
||||||
|
// Convert degrees to radians
|
||||||
|
const a = degreesToRadians(degrees);
|
||||||
|
|
||||||
|
const c = Math.cos(a);
|
||||||
|
const s = Math.sin(a);
|
||||||
|
|
||||||
|
// rotation matrix
|
||||||
|
const matrix = [
|
||||||
|
0.213 + c * 0.787 - s * 0.213,
|
||||||
|
0.715 - c * 0.715 - s * 0.715,
|
||||||
|
0.072 - c * 0.072 + s * 0.928,
|
||||||
|
0.213 - c * 0.213 + s * 0.143,
|
||||||
|
0.715 + c * 0.285 + s * 0.14,
|
||||||
|
0.072 - c * 0.072 - s * 0.283,
|
||||||
|
0.213 - c * 0.213 - s * 0.787,
|
||||||
|
0.715 - c * 0.715 + s * 0.715,
|
||||||
|
0.072 + c * 0.928 + s * 0.072,
|
||||||
|
];
|
||||||
|
|
||||||
|
// transform
|
||||||
|
const newR = r * matrix[0] + g * matrix[1] + b * matrix[2];
|
||||||
|
const newG = r * matrix[3] + g * matrix[4] + b * matrix[5];
|
||||||
|
const newB = r * matrix[6] + g * matrix[7] + b * matrix[8];
|
||||||
|
|
||||||
|
// clamp the values to [0, 1] range and convert back to [0, 255]
|
||||||
|
return {
|
||||||
|
r: Math.round(Math.max(0, Math.min(1, newR)) * 255),
|
||||||
|
g: Math.round(Math.max(0, Math.min(1, newG)) * 255),
|
||||||
|
b: Math.round(Math.max(0, Math.min(1, newB)) * 255),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssInvert = (
|
||||||
|
r: number,
|
||||||
|
g: number,
|
||||||
|
b: number,
|
||||||
|
percent: number,
|
||||||
|
): { r: number; g: number; b: number } => {
|
||||||
|
const p = clamp(percent, 0, 100) / 100;
|
||||||
|
|
||||||
|
// Function to invert a single color component
|
||||||
|
const invertComponent = (color: number): number => {
|
||||||
|
// Apply the invert formula
|
||||||
|
const inverted = color * (1 - p) + (255 - color) * p;
|
||||||
|
// Round to the nearest integer and clamp to [0, 255]
|
||||||
|
return Math.round(clamp(inverted, 0, 255));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the inverted RGB components
|
||||||
|
const invertedR = invertComponent(r);
|
||||||
|
const invertedG = invertComponent(g);
|
||||||
|
const invertedB = invertComponent(b);
|
||||||
|
|
||||||
|
return { r: invertedR, g: invertedG, b: invertedB };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyDarkModeFilter = (color: string) => {
|
||||||
|
let tc = tinycolor(color);
|
||||||
|
|
||||||
|
const _alpha = tc._a;
|
||||||
|
|
||||||
|
// order of operations matters
|
||||||
|
// (corresponds to "filter: invert(invertPercent) hue-rotate(hueDegrees)" in css)
|
||||||
|
tc = tinycolor(cssInvert(tc._r, tc._g, tc._b, 93));
|
||||||
|
tc = tinycolor(cssHueRotate(tc._r, tc._g, tc._b, 180 as Degrees));
|
||||||
|
tc.setAlpha(_alpha);
|
||||||
|
|
||||||
|
return tc.toHex8String();
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ import type {
|
|||||||
ElementsPendingErasure,
|
ElementsPendingErasure,
|
||||||
GenerateDiagramToCode,
|
GenerateDiagramToCode,
|
||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
|
Offsets,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@@ -440,6 +441,7 @@ import {
|
|||||||
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 +550,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 +558,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 +685,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 +1484,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();
|
||||||
|
|
||||||
@@ -1695,6 +1700,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure: this.elementsPendingErasure,
|
elementsPendingErasure: this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes:
|
pendingFlowchartNodes:
|
||||||
this.flowChartCreator.pendingNodes,
|
this.flowChartCreator.pendingNodes,
|
||||||
|
theme: this.state.theme,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{this.state.newElement && (
|
{this.state.newElement && (
|
||||||
@@ -1715,6 +1721,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure:
|
elementsPendingErasure:
|
||||||
this.elementsPendingErasure,
|
this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
|
theme: this.state.theme,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -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 =
|
||||||
@@ -2677,6 +2697,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (prevState.theme !== this.state.theme) {
|
||||||
|
this.scene
|
||||||
|
.getElementsIncludingDeleted()
|
||||||
|
.forEach((element) => ShapeCache.delete(element));
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.state.activeTool.type === "eraser" &&
|
this.state.activeTool.type === "eraser" &&
|
||||||
prevState.theme !== this.state.theme
|
prevState.theme !== this.state.theme
|
||||||
@@ -3215,6 +3240,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 +3553,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,6 +3570,11 @@ 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 +3588,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 +3708,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 +3822,42 @@ 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;
|
||||||
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
|
||||||
);
|
|
||||||
const propertiesPanelWidth = Math.max(
|
|
||||||
this.excalidrawContainerRef?.current
|
|
||||||
?.querySelector(".App-menu__left")
|
?.querySelector(".App-menu__left")
|
||||||
?.getBoundingClientRect()?.width ?? 0,
|
?.getBoundingClientRect();
|
||||||
0,
|
|
||||||
);
|
const PADDING = 16;
|
||||||
|
|
||||||
return getLanguage().rtl
|
return getLanguage().rtl
|
||||||
? {
|
? {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: propertiesPanelWidth,
|
right:
|
||||||
bottom: 0,
|
Math.max(
|
||||||
left: sidebarWidth,
|
this.state.width -
|
||||||
|
(propertiesPanelRect?.left ?? this.state.width),
|
||||||
|
0,
|
||||||
|
) + PADDING,
|
||||||
|
bottom: PADDING,
|
||||||
|
left: Math.max(sidebarRect?.right ?? 0, 0) + PADDING,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
top: toolbarBottom,
|
top: toolbarBottom + PADDING,
|
||||||
right: sidebarWidth,
|
right: Math.max(
|
||||||
bottom: 0,
|
this.state.width -
|
||||||
left: propertiesPanelWidth,
|
(sidebarRect?.left ?? this.state.width) +
|
||||||
|
PADDING,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
bottom: PADDING,
|
||||||
|
left: Math.max(propertiesPanelRect?.right ?? 0, 0) + PADDING,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3914,7 +3942,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 +3998,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 +4431,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 +5999,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 +6437,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
||||||
if (!this.state.editingTextElement) {
|
// due to event.preventDefault below, container wouldn't get focus
|
||||||
|
// automatically
|
||||||
|
this.focusContainer();
|
||||||
|
|
||||||
// preventing defualt while text editing messes with cursor/focus
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
|
if (!this.state.editingTextElement) {
|
||||||
|
// 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",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
paddingRight: "1em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("toolBar.library")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DefaultSidebarTabTriggersTunnel.Out />
|
<DefaultSidebarTabTriggersTunnel.Out />
|
||||||
|
</Sidebar.TabTriggers>
|
||||||
</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",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
filter: var(--theme-filter);
|
// filter: var(--theme-filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -189,7 +189,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)"; // prod
|
||||||
|
// $theme-filter: "invert(93%)"; // prod
|
||||||
|
// $theme-filter: "hue-rotate(180deg)"; // prod
|
||||||
|
// $theme-filter: "hue-rotate(180deg) invert(93%)";
|
||||||
|
|
||||||
$right-sidebar-width: "302px";
|
$right-sidebar-width: "302px";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
|
|||||||
@@ -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
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
|
import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@@ -247,7 +248,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
|
||||||
@@ -273,10 +274,15 @@ export const textWysiwyg = ({
|
|||||||
textAlign,
|
textAlign,
|
||||||
verticalAlign,
|
verticalAlign,
|
||||||
color: updatedTextElement.strokeColor,
|
color: updatedTextElement.strokeColor,
|
||||||
|
// color:
|
||||||
|
// appState.theme === THEME.DARK
|
||||||
|
// ? applyDarkModeFilter(updatedTextElement.strokeColor)
|
||||||
|
// : updatedTextElement.strokeColor,
|
||||||
opacity: updatedTextElement.opacity / 100,
|
opacity: updatedTextElement.opacity / 100,
|
||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
|
// console.log("...", updatedTextElement.strokeColor);
|
||||||
editable.scrollTop = 0;
|
editable.scrollTop = 0;
|
||||||
// For some reason updating font attribute doesn't set font family
|
// For some reason updating font attribute doesn't set font family
|
||||||
// hence updating font family explicitly for test environment
|
// hence updating font family explicitly for test environment
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Vendored
+10
@@ -104,3 +104,13 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace tinycolor {
|
||||||
|
interface Instance {
|
||||||
|
_r: number;
|
||||||
|
_g: number;
|
||||||
|
_b: number;
|
||||||
|
_a: number;
|
||||||
|
_ok: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
|
"@types/tinycolor2": "1.4.6",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"roughjs": "4.6.4",
|
"roughjs": "4.6.4",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
|
"tinycolor2": "1.6.0",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { StaticCanvasAppState, AppState } from "../types";
|
|||||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
|
|
||||||
import { THEME, THEME_FILTER } from "../constants";
|
import { THEME, THEME_FILTER } from "../constants";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
export const fillCircle = (
|
export const fillCircle = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -50,7 +51,7 @@ export const bootstrapCanvas = ({
|
|||||||
context.scale(scale, scale);
|
context.scale(scale, scale);
|
||||||
|
|
||||||
if (isExporting && theme === THEME.DARK) {
|
if (isExporting && theme === THEME.DARK) {
|
||||||
context.filter = THEME_FILTER;
|
// context.filter = THEME_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint background
|
// Paint background
|
||||||
@@ -64,7 +65,10 @@ export const bootstrapCanvas = ({
|
|||||||
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
}
|
}
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = viewBackgroundColor;
|
context.fillStyle =
|
||||||
|
theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(viewBackgroundColor)
|
||||||
|
: viewBackgroundColor;
|
||||||
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
@@ -952,9 +956,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({
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { ShapeCache } from "../scene/ShapeCache";
|
|||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { isRightAngleRads } from "../../math";
|
import { isRightAngleRads } from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@@ -247,9 +248,9 @@ const generateElementCanvas = (
|
|||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
// in dark theme, revert the image color filter
|
// in dark theme, revert the image color filter
|
||||||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
// if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = IMAGE_INVERT_FILTER;
|
// context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
// }
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
|
|
||||||
@@ -403,7 +404,10 @@ const drawElementOnCanvas = (
|
|||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
// Draw directly to canvas
|
// Draw directly to canvas
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
|
|
||||||
const path = getFreeDrawPath2D(element) as Path2D;
|
const path = getFreeDrawPath2D(element) as Path2D;
|
||||||
const fillShape = ShapeCache.get(element);
|
const fillShape = ShapeCache.get(element);
|
||||||
@@ -412,7 +416,10 @@ const drawElementOnCanvas = (
|
|||||||
rc.draw(fillShape);
|
rc.draw(fillShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
context.fill(path);
|
context.fill(path);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@@ -458,7 +465,10 @@ const drawElementOnCanvas = (
|
|||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
context.save();
|
context.save();
|
||||||
context.font = getFontString(element);
|
context.font = getFontString(element);
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
@@ -699,7 +709,10 @@ export const renderElement = (
|
|||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||||
context.strokeStyle = FRAME_STYLE.strokeColor;
|
context.strokeStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: FRAME_STYLE.strokeColor;
|
||||||
|
|
||||||
// TODO change later to only affect AI frames
|
// TODO change later to only affect AI frames
|
||||||
if (isMagicFrameElement(element)) {
|
if (isMagicFrameElement(element)) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
|
THEME,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { normalizeLink, toValidURL } from "../data/url";
|
import { normalizeLink, toValidURL } from "../data/url";
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
@@ -37,6 +38,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
|||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
@@ -139,7 +141,7 @@ const renderElementToSvg = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
const shape = ShapeCache.generateElementShape(element, null);
|
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@@ -389,7 +391,12 @@ const renderElementToSvg = (
|
|||||||
);
|
);
|
||||||
node.setAttribute("stroke", "none");
|
node.setAttribute("stroke", "none");
|
||||||
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||||
path.setAttribute("fill", element.strokeColor);
|
path.setAttribute(
|
||||||
|
"fill",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
|
);
|
||||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||||
node.appendChild(path);
|
node.appendChild(path);
|
||||||
|
|
||||||
@@ -421,6 +428,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);
|
||||||
|
|
||||||
@@ -525,7 +533,12 @@ const renderElementToSvg = (
|
|||||||
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
||||||
|
|
||||||
rect.setAttribute("fill", "none");
|
rect.setAttribute("fill", "none");
|
||||||
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
|
rect.setAttribute(
|
||||||
|
"stroke",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(FRAME_STYLE.strokeColor)
|
||||||
|
: FRAME_STYLE.strokeColor,
|
||||||
|
);
|
||||||
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
||||||
|
|
||||||
addToRoot(rect, element);
|
addToRoot(rect, element);
|
||||||
@@ -576,7 +589,12 @@ const renderElementToSvg = (
|
|||||||
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
||||||
text.setAttribute("font-family", getFontFamilyString(element));
|
text.setAttribute("font-family", getFontFamilyString(element));
|
||||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
text.setAttribute("fill", element.strokeColor);
|
text.setAttribute(
|
||||||
|
"fill",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
|
);
|
||||||
text.setAttribute("text-anchor", textAnchor);
|
text.setAttribute("text-anchor", textAnchor);
|
||||||
text.setAttribute("style", "white-space: pre;");
|
text.setAttribute("style", "white-space: pre;");
|
||||||
text.setAttribute("direction", direction);
|
text.setAttribute("direction", direction);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
import { ROUGHNESS, THEME } from "../constants";
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||||
import {
|
import {
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ function adjustRoughness(element: ExcalidrawElement): number {
|
|||||||
export const generateRoughOptions = (
|
export const generateRoughOptions = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
continuousPath = false,
|
continuousPath = false,
|
||||||
|
isDarkMode: boolean = false,
|
||||||
): Options => {
|
): Options => {
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
seed: element.seed,
|
seed: element.seed,
|
||||||
@@ -85,7 +87,9 @@ export const generateRoughOptions = (
|
|||||||
fillWeight: element.strokeWidth / 2,
|
fillWeight: element.strokeWidth / 2,
|
||||||
hachureGap: element.strokeWidth * 4,
|
hachureGap: element.strokeWidth * 4,
|
||||||
roughness: adjustRoughness(element),
|
roughness: adjustRoughness(element),
|
||||||
stroke: element.strokeColor,
|
stroke: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
preserveVertices:
|
preserveVertices:
|
||||||
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
||||||
};
|
};
|
||||||
@@ -99,6 +103,8 @@ export const generateRoughOptions = (
|
|||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill = isTransparent(element.backgroundColor)
|
options.fill = isTransparent(element.backgroundColor)
|
||||||
? undefined
|
? undefined
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.backgroundColor)
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
options.curveFitting = 1;
|
options.curveFitting = 1;
|
||||||
@@ -112,6 +118,8 @@ export const generateRoughOptions = (
|
|||||||
options.fill =
|
options.fill =
|
||||||
element.backgroundColor === "transparent"
|
element.backgroundColor === "transparent"
|
||||||
? undefined
|
? undefined
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.backgroundColor)
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
@@ -165,6 +173,7 @@ const getArrowheadShapes = (
|
|||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
options: Options,
|
options: Options,
|
||||||
canvasBackgroundColor: string,
|
canvasBackgroundColor: string,
|
||||||
|
isDarkMode: boolean,
|
||||||
) => {
|
) => {
|
||||||
const arrowheadPoints = getArrowheadPoints(
|
const arrowheadPoints = getArrowheadPoints(
|
||||||
element,
|
element,
|
||||||
@@ -192,10 +201,14 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "circle_outline"
|
arrowhead === "circle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
|
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
stroke: element.strokeColor,
|
stroke: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
roughness: Math.min(0.5, options.roughness || 0),
|
roughness: Math.min(0.5, options.roughness || 0),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -220,6 +233,8 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "triangle_outline"
|
arrowhead === "triangle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@@ -248,6 +263,8 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "diamond_outline"
|
arrowhead === "diamond_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@@ -291,12 +308,15 @@ export const _generateElementShape = (
|
|||||||
isExporting,
|
isExporting,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
|
theme,
|
||||||
}: {
|
}: {
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: string;
|
canvasBackgroundColor: string;
|
||||||
embedsValidationStatus: EmbedsValidationStatus | null;
|
embedsValidationStatus: EmbedsValidationStatus | null;
|
||||||
|
theme: AppState["theme"];
|
||||||
},
|
},
|
||||||
): Drawable | Drawable[] | null => {
|
): Drawable | Drawable[] | null => {
|
||||||
|
const isDarkMode = theme === THEME.DARK;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
@@ -322,6 +342,7 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
|
isDarkMode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -337,6 +358,7 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
|
isDarkMode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -374,7 +396,7 @@ export const _generateElementShape = (
|
|||||||
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
||||||
topY + horizontalRadius
|
topY + horizontalRadius
|
||||||
}`,
|
}`,
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true, isDarkMode),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
shape = generator.polygon(
|
shape = generator.polygon(
|
||||||
@@ -384,7 +406,7 @@ export const _generateElementShape = (
|
|||||||
[bottomX, bottomY],
|
[bottomX, bottomY],
|
||||||
[leftX, leftY],
|
[leftX, leftY],
|
||||||
],
|
],
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element, undefined, isDarkMode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return shape;
|
return shape;
|
||||||
@@ -395,14 +417,14 @@ export const _generateElementShape = (
|
|||||||
element.height / 2,
|
element.height / 2,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element, undefined, isDarkMode),
|
||||||
);
|
);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
let shape: ElementShapes[typeof element.type];
|
let shape: ElementShapes[typeof element.type];
|
||||||
const options = generateRoughOptions(element);
|
const options = generateRoughOptions(element, undefined, isDarkMode);
|
||||||
|
|
||||||
// points array can be empty in the beginning, so it is important to add
|
// points array can be empty in the beginning, so it is important to add
|
||||||
// initial position to it
|
// initial position to it
|
||||||
@@ -414,7 +436,7 @@ export const _generateElementShape = (
|
|||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points, 16),
|
generateElbowArrowShape(points, 16),
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true, isDarkMode),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
} else if (!element.roundness) {
|
} else if (!element.roundness) {
|
||||||
@@ -446,6 +468,7 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
|
isDarkMode,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@@ -463,6 +486,7 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
|
isDarkMode,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@@ -477,7 +501,7 @@ export const _generateElementShape = (
|
|||||||
// generate rough polygon to fill freedraw shape
|
// generate rough polygon to fill freedraw shape
|
||||||
const simplifiedPoints = simplify(element.points, 0.75);
|
const simplifiedPoints = simplify(element.points, 0.75);
|
||||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||||
...generateRoughOptions(element),
|
...generateRoughOptions(element, undefined, isDarkMode),
|
||||||
stroke: "none",
|
stroke: "none",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { _generateElementShape } from "./Shape";
|
|||||||
import type { ElementShape, ElementShapes } from "./types";
|
import type { ElementShape, ElementShapes } from "./types";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "../colors";
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||||
|
import { THEME } from "..";
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
||||||
private static rg = new RoughGenerator();
|
private static rg = new RoughGenerator();
|
||||||
@@ -52,6 +53,7 @@ export class ShapeCache {
|
|||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
|
theme: AppState["theme"];
|
||||||
} | null,
|
} | null,
|
||||||
) => {
|
) => {
|
||||||
// when exporting, always regenerated to guarantee the latest shape
|
// when exporting, always regenerated to guarantee the latest shape
|
||||||
@@ -74,6 +76,7 @@ export class ShapeCache {
|
|||||||
isExporting: false,
|
isExporting: false,
|
||||||
canvasBackgroundColor: COLOR_PALETTE.white,
|
canvasBackgroundColor: COLOR_PALETTE.white,
|
||||||
embedsValidationStatus: null,
|
embedsValidationStatus: null,
|
||||||
|
theme: THEME.LIGHT,
|
||||||
},
|
},
|
||||||
) as T["type"] extends keyof ElementShapes
|
) as T["type"] extends keyof ElementShapes
|
||||||
? ElementShapes[T["type"]]
|
? ElementShapes[T["type"]]
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
|
|||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import type { Font } from "../fonts/ExcalidrawFont";
|
import type { Font } from "../fonts/ExcalidrawFont";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@@ -214,6 +215,8 @@ export const exportToCanvas = async (
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
||||||
|
|
||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
canvas,
|
canvas,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
@@ -233,7 +236,7 @@ export const exportToCanvas = async (
|
|||||||
scrollY: -minY + exportPadding,
|
scrollY: -minY + exportPadding,
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
|
theme,
|
||||||
},
|
},
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
canvasBackgroundColor: viewBackgroundColor,
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
@@ -244,6 +247,7 @@ export const exportToCanvas = async (
|
|||||||
embedsValidationStatus: new Map(),
|
embedsValidationStatus: new Map(),
|
||||||
elementsPendingErasure: new Set(),
|
elementsPendingErasure: new Set(),
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,7 +334,7 @@ export const exportToSvg = async (
|
|||||||
svgRoot.setAttribute("width", `${width * exportScale}`);
|
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||||
svgRoot.setAttribute("height", `${height * exportScale}`);
|
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||||
if (exportWithDarkMode) {
|
if (exportWithDarkMode) {
|
||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
// svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = -minX + exportPadding;
|
const offsetX = -minX + exportPadding;
|
||||||
@@ -376,7 +380,12 @@ export const exportToSvg = async (
|
|||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", `${width}`);
|
rect.setAttribute("width", `${width}`);
|
||||||
rect.setAttribute("height", `${height}`);
|
rect.setAttribute("height", `${height}`);
|
||||||
rect.setAttribute("fill", viewBackgroundColor);
|
rect.setAttribute(
|
||||||
|
"fill",
|
||||||
|
appState.exportWithDarkMode
|
||||||
|
? applyDarkModeFilter(viewBackgroundColor)
|
||||||
|
: viewBackgroundColor,
|
||||||
|
);
|
||||||
svgRoot.appendChild(rect);
|
svgRoot.appendChild(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +393,8 @@ export const exportToSvg = async (
|
|||||||
|
|
||||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||||
|
|
||||||
|
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
||||||
|
|
||||||
renderSceneToSvg(
|
renderSceneToSvg(
|
||||||
elementsForRender,
|
elementsForRender,
|
||||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||||
@@ -405,6 +416,7 @@ export const exportToSvg = async (
|
|||||||
.map((element) => [element.id, true]),
|
.map((element) => [element.id, true]),
|
||||||
)
|
)
|
||||||
: new Map(),
|
: new Map(),
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type StaticCanvasRenderConfig = {
|
|||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
elementsPendingErasure: ElementsPendingErasure;
|
elementsPendingErasure: ElementsPendingErasure;
|
||||||
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
||||||
|
theme: AppState["theme"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SVGRenderConfig = {
|
export type SVGRenderConfig = {
|
||||||
@@ -46,6 +47,7 @@ export type SVGRenderConfig = {
|
|||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
|
theme: AppState["theme"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InteractiveCanvasRenderConfig = {
|
export type InteractiveCanvasRenderConfig = {
|
||||||
|
|||||||
@@ -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,6 +8423,7 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -8613,6 +8642,7 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -8830,6 +8860,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,6 +9051,7 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -9238,6 +9270,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,6 +9447,7 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -9631,6 +9665,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 +9842,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 +10033,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 +10214,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 +10723,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 +10995,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 +11120,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 +11317,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 +11626,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 +12036,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 +12644,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 +12769,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 +13351,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 +13685,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 +13945,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 +14068,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 +14444,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 +14570,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
|
||||||
|
|||||||
@@ -76,11 +76,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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 } });
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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;
|
||||||
|
}>;
|
||||||
|
|||||||
+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(),
|
||||||
|
|||||||
+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
-4
@@ -2,19 +2,27 @@ import { defineConfig } from "vitest/config";
|
|||||||
import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins";
|
import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// @ts-ignore
|
//@ts-ignore
|
||||||
plugins: [woff2BrowserPlugin()],
|
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: 66,
|
||||||
branches: 70,
|
branches: 70,
|
||||||
functions: 68,
|
functions: 63,
|
||||||
statements: 70,
|
statements: 66,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user