Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e163402e22 |
+1
-1
@@ -22,7 +22,7 @@ VITE_APP_DEV_ENABLE_SW=
|
|||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
VITE_APP_ENABLE_TRACKING=true
|
VITE_APP_DISABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@ VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
|||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
VITE_APP_ENABLE_TRACKING=false
|
VITE_APP_DISABLE_TRACKING=
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on: pull_request
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
|
|||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||||
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />;
|
return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function App() {
|
|||||||
<img src={canvasUrl} alt="" />
|
<img src={canvasUrl} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "400px" }}>
|
<div style={{ height: "400px" }}>
|
||||||
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+23
-6
@@ -1,4 +1,5 @@
|
|||||||
import polyfill from "../packages/excalidraw/polyfill";
|
import polyfill from "../packages/excalidraw/polyfill";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../packages/excalidraw/analytics";
|
import { trackEvent } from "../packages/excalidraw/analytics";
|
||||||
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
||||||
@@ -21,6 +22,7 @@ import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRef
|
|||||||
import { t } from "../packages/excalidraw/i18n";
|
import { t } from "../packages/excalidraw/i18n";
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
|
defaultLang,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
TTDDialog,
|
TTDDialog,
|
||||||
TTDDialogTrigger,
|
TTDDialogTrigger,
|
||||||
@@ -91,7 +93,7 @@ import {
|
|||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { AppFooter } from "./components/AppFooter";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
import { Provider, useAtom, useAtomValue } from "jotai";
|
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||||
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
|
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
|
||||||
import { appJotaiStore } from "./app-jotai";
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
@@ -119,8 +121,6 @@ import {
|
|||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "../packages/excalidraw/components/icons";
|
} from "../packages/excalidraw/components/icons";
|
||||||
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
||||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
|
||||||
import { useAppLangCode } from "./app-language/language-state";
|
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -172,6 +172,11 @@ if (window.self !== window.top) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const languageDetector = new LanguageDetector();
|
||||||
|
languageDetector.init({
|
||||||
|
languageUtils: {},
|
||||||
|
});
|
||||||
|
|
||||||
const shareableLinkConfirmDialog = {
|
const shareableLinkConfirmDialog = {
|
||||||
title: t("overwriteConfirm.modal.shareableLink.title"),
|
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||||
description: (
|
description: (
|
||||||
@@ -317,15 +322,19 @@ const initializeScene = async (opts: {
|
|||||||
return { scene: null, isExternalScene: false };
|
return { scene: null, isExternalScene: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||||
|
export const appLangCodeAtom = atom(
|
||||||
|
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||||
|
);
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
const isCollabDisabled = isRunningInIframe();
|
const isCollabDisabled = isRunningInIframe();
|
||||||
|
|
||||||
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
||||||
const { editorTheme } = useHandleAppTheme();
|
const { editorTheme } = useHandleAppTheme();
|
||||||
|
|
||||||
const [langCode, setLangCode] = useAppLangCode();
|
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -481,7 +490,11 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||||
const localDataState = importFromLocalStorage();
|
const localDataState = importFromLocalStorage();
|
||||||
const username = importUsernameFromLocalStorage();
|
const username = importUsernameFromLocalStorage();
|
||||||
setLangCode(getPreferredLanguage());
|
let langCode = languageDetector.detect() || defaultLang.code;
|
||||||
|
if (Array.isArray(langCode)) {
|
||||||
|
langCode = langCode[0];
|
||||||
|
}
|
||||||
|
setLangCode(langCode);
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...localDataState,
|
...localDataState,
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
@@ -582,6 +595,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
};
|
};
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
languageDetector.cacheUserLanguage(langCode);
|
||||||
|
}, [langCode]);
|
||||||
|
|
||||||
const onChange = (
|
const onChange = (
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import { defaultLang, languages } from "../../packages/excalidraw";
|
|
||||||
|
|
||||||
export const languageDetector = new LanguageDetector();
|
|
||||||
|
|
||||||
languageDetector.init({
|
|
||||||
languageUtils: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getPreferredLanguage = () => {
|
|
||||||
const detectedLanguages = languageDetector.detect();
|
|
||||||
|
|
||||||
const detectedLanguage = Array.isArray(detectedLanguages)
|
|
||||||
? detectedLanguages[0]
|
|
||||||
: detectedLanguages;
|
|
||||||
|
|
||||||
const initialLanguage =
|
|
||||||
(detectedLanguage
|
|
||||||
? // region code may not be defined if user uses generic preferred language
|
|
||||||
// (e.g. chinese vs instead of chienese-simplified)
|
|
||||||
languages.find((lang) => lang.code.startsWith(detectedLanguage))?.code
|
|
||||||
: null) || defaultLang.code;
|
|
||||||
|
|
||||||
return initialLanguage;
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
|
||||||
|
|
||||||
export const appLangCodeAtom = atom(getPreferredLanguage());
|
|
||||||
|
|
||||||
export const useAppLangCode = () => {
|
|
||||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
languageDetector.cacheUserLanguage(langCode);
|
|
||||||
}, [langCode]);
|
|
||||||
|
|
||||||
return [langCode, setLangCode] as const;
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import type { Theme } from "../../packages/excalidraw/element/types";
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "./LanguageList";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
@@ -34,7 +34,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
<MainMenu.ItemLink
|
<MainMenu.ItemLink
|
||||||
icon={ExcalLogo}
|
icon={ExcalLogo}
|
||||||
href={`${
|
href={`${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useI18n, languages } from "../../packages/excalidraw/i18n";
|
import { appLangCodeAtom } from "../App";
|
||||||
import { appLangCodeAtom } from "./language-state";
|
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||||
|
import { languages } from "../../packages/excalidraw/i18n";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const { t, langCode } = useI18n();
|
const { t, langCode } = useI18n();
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta name="image" content="https://excalidraw.com/og-image-3.png" />
|
<meta name="image" content="https://excalidraw.com/og-image-2.png" />
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:site_name" content="Excalidraw" />
|
<meta property="og:site_name" content="Excalidraw" />
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
property="og:description"
|
property="og:description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="https://excalidraw.com/og-image-3.png" />
|
<meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="twitter:image"
|
property="twitter:image"
|
||||||
content="https://excalidraw.com/og-image-3.png"
|
content="https://excalidraw.com/og-twitter-v2.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- General tags -->
|
<!-- General tags -->
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
margin-inline-end: 0.6em;
|
margin-inline-end: 0.6em;
|
||||||
z-index: var(--zIndex-layerUI);
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build",
|
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
||||||
"build:version": "node ../scripts/build-version.js",
|
"build:version": "node ../scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"start": "yarn && vite",
|
"start": "yarn && vite",
|
||||||
|
|||||||
@@ -64,12 +64,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
workbox: {
|
workbox: {
|
||||||
// Don't push fonts and locales to app precache
|
// Don't push fonts and locales to app precache
|
||||||
globIgnores: [
|
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
|
||||||
"fonts.css",
|
|
||||||
"**/locales/**",
|
|
||||||
"service-worker.js",
|
|
||||||
"lz-string",
|
|
||||||
],
|
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
|
||||||
|
|
||||||
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
||||||
|
|
||||||
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
|
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const actionClearCanvas = register({
|
|||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
stats: appState.stats,
|
showStats: appState.showStats,
|
||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
activeTool:
|
activeTool:
|
||||||
appState.activeTool.type === "image"
|
appState.activeTool.type === "image"
|
||||||
|
|||||||
@@ -131,12 +131,7 @@ export const actionFinalize = register({
|
|||||||
-1,
|
-1,
|
||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
maybeBindLinearElement(
|
maybeBindLinearElement(multiPointElement, appState, { x, y }, app);
|
||||||
multiPointElement,
|
|
||||||
appState,
|
|
||||||
{ x, y },
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const flipElements = (
|
|||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
elementsMap,
|
app,
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,22 +5,21 @@ import { StoreAction } from "../store";
|
|||||||
|
|
||||||
export const actionToggleStats = register({
|
export const actionToggleStats = register({
|
||||||
name: "stats",
|
name: "stats",
|
||||||
label: "stats.fullTitle",
|
label: "stats.title",
|
||||||
icon: abacusIcon,
|
icon: abacusIcon,
|
||||||
paletteName: "Toggle stats",
|
paletteName: "Toggle stats",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "menu" },
|
trackEvent: { category: "menu" },
|
||||||
keywords: ["edit", "attributes", "customize"],
|
|
||||||
perform(elements, appState) {
|
perform(elements, appState) {
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
stats: { ...appState.stats, open: !this.checked!(appState) },
|
showStats: !this.checked!(appState),
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.NONE,
|
storeAction: StoreAction.NONE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState) => appState.stats.open,
|
checked: (appState) => appState.showStats,
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -135,8 +135,7 @@ export type ActionName =
|
|||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer"
|
| "wrapTextInContainer"
|
||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "autoResize"
|
| "autoResize";
|
||||||
| "elementStats";
|
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// place here categories that you want to track. We want to track just a
|
// place here categories that you want to track. We want to track just a
|
||||||
// small subset of categories at a given time.
|
// small subset of categories at a given time.
|
||||||
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette"]);
|
const ALLOWED_CATEGORIES_TO_TRACK = ["ai", "command_palette"] as string[];
|
||||||
|
|
||||||
export const trackEvent = (
|
export const trackEvent = (
|
||||||
category: string,
|
category: string,
|
||||||
@@ -9,20 +9,17 @@ export const trackEvent = (
|
|||||||
value?: number,
|
value?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
// prettier-ignore
|
||||||
if (
|
if (
|
||||||
typeof window === "undefined" ||
|
typeof window === "undefined"
|
||||||
import.meta.env.VITE_WORKER_ID ||
|
|| import.meta.env.VITE_WORKER_ID
|
||||||
import.meta.env.VITE_APP_ENABLE_TRACKING !== "true"
|
// comment out to debug locally
|
||||||
|
|| import.meta.env.PROD
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ALLOWED_CATEGORIES_TO_TRACK.has(category)) {
|
if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
// comment out to debug in dev
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
EXPORT_SCALES,
|
EXPORT_SCALES,
|
||||||
STATS_PANELS,
|
|
||||||
THEME,
|
THEME,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { AppState, NormalizedZoomValue } from "./types";
|
import type { AppState, NormalizedZoomValue } from "./types";
|
||||||
@@ -81,10 +80,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
selectedElementsAreBeingDragged: false,
|
selectedElementsAreBeingDragged: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
stats: {
|
showStats: false,
|
||||||
open: false,
|
|
||||||
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
|
||||||
},
|
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
@@ -200,7 +196,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
},
|
},
|
||||||
selectionElement: { browser: false, export: false, server: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
stats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
frameRendering: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -1477,28 +1477,19 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unordered = Array.from(elements.values());
|
const previous = Array.from(elements.values());
|
||||||
const ordered = orderByFractionalIndex([...unordered]);
|
const reordered = orderByFractionalIndex([...previous]);
|
||||||
const moved = Delta.getRightDifferences(unordered, ordered, true).reduce(
|
|
||||||
(acc, arrayIndex) => {
|
|
||||||
const candidate = unordered[Number(arrayIndex)];
|
|
||||||
if (candidate && changed.has(candidate.id)) {
|
|
||||||
acc.set(candidate.id, candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
if (
|
||||||
},
|
!flags.containsVisibleDifference &&
|
||||||
new Map(),
|
Delta.isRightDifferent(previous, reordered, true)
|
||||||
);
|
) {
|
||||||
|
|
||||||
if (!flags.containsVisibleDifference && moved.size) {
|
|
||||||
// we found a difference in order!
|
// we found a difference in order!
|
||||||
flags.containsVisibleDifference = true;
|
flags.containsVisibleDifference = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// synchronize all elements that were actually moved
|
// let's synchronize all invalid indices of moved elements
|
||||||
// could fallback to synchronizing all invalid indices
|
return arrayToMap(syncMovedIndices(reordered, changed)) as typeof elements;
|
||||||
return arrayToMap(syncMovedIndices(ordered, moved)) as typeof elements;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ import {
|
|||||||
isIOS,
|
isIOS,
|
||||||
supportsResizeObserver,
|
supportsResizeObserver,
|
||||||
DEFAULT_COLLISION_THRESHOLD,
|
DEFAULT_COLLISION_THRESHOLD,
|
||||||
DEFAULT_TEXT_ALIGN,
|
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { ExportedElements } from "../data";
|
import type { ExportedElements } from "../data";
|
||||||
import { exportCanvas, loadFromBlob } from "../data";
|
import { exportCanvas, loadFromBlob } from "../data";
|
||||||
@@ -224,9 +223,16 @@ import type {
|
|||||||
ScrollBars,
|
ScrollBars,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { findShapeByKey, getElementShape } from "../shapes";
|
import { findShapeByKey } from "../shapes";
|
||||||
import type { GeometricShape } from "../../utils/geometry/shape";
|
import type { GeometricShape } from "../../utils/geometry/shape";
|
||||||
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
import {
|
||||||
|
getClosedCurveShape,
|
||||||
|
getCurveShape,
|
||||||
|
getEllipseShape,
|
||||||
|
getFreedrawShape,
|
||||||
|
getPolygonShape,
|
||||||
|
getSelectionBoxShape,
|
||||||
|
} from "../../utils/geometry/shape";
|
||||||
import { isPointInShape } from "../../utils/collision";
|
import { isPointInShape } from "../../utils/collision";
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
@@ -323,11 +329,8 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getMinTextElementWidth,
|
|
||||||
isMeasureTextSupported,
|
isMeasureTextSupported,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
measureText,
|
|
||||||
wrapText,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
showHyperlinkTooltip,
|
showHyperlinkTooltip,
|
||||||
@@ -416,6 +419,7 @@ import {
|
|||||||
hitElementBoundText,
|
hitElementBoundText,
|
||||||
hitElementBoundingBoxOnly,
|
hitElementBoundingBoxOnly,
|
||||||
hitElementItself,
|
hitElementItself,
|
||||||
|
shouldTestInside,
|
||||||
} from "../element/collision";
|
} from "../element/collision";
|
||||||
import { textWysiwyg } from "../element/textWysiwyg";
|
import { textWysiwyg } from "../element/textWysiwyg";
|
||||||
import { isOverScrollBars } from "../scene/scrollbars";
|
import { isOverScrollBars } from "../scene/scrollbars";
|
||||||
@@ -426,8 +430,6 @@ import {
|
|||||||
} from "./hyperlink/helpers";
|
} from "./hyperlink/helpers";
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
||||||
import { getVisibleSceneBounds } from "../element/bounds";
|
|
||||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -1690,7 +1692,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
canvas={this.interactiveCanvas}
|
canvas={this.interactiveCanvas}
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
visibleElements={visibleElements}
|
visibleElements={visibleElements}
|
||||||
allElementsMap={allElementsMap}
|
|
||||||
selectedElements={selectedElements}
|
selectedElements={selectedElements}
|
||||||
sceneNonce={sceneNonce}
|
sceneNonce={sceneNonce}
|
||||||
selectionNonce={
|
selectionNonce={
|
||||||
@@ -2126,96 +2127,95 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public syncActionResult = withBatchedUpdates((actionResult: ActionResult) => {
|
private syncActionResult = withBatchedUpdates(
|
||||||
if (this.unmounted || actionResult === false) {
|
(actionResult: ActionResult) => {
|
||||||
return;
|
if (this.unmounted || actionResult === false) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
if (actionResult.storeAction === StoreAction.UPDATE) {
|
|
||||||
this.store.shouldUpdateSnapshot();
|
|
||||||
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
|
|
||||||
this.store.shouldCaptureIncrement();
|
|
||||||
}
|
|
||||||
|
|
||||||
let didUpdate = false;
|
|
||||||
|
|
||||||
let editingElement: AppState["editingElement"] | null = null;
|
|
||||||
if (actionResult.elements) {
|
|
||||||
actionResult.elements.forEach((element) => {
|
|
||||||
if (
|
|
||||||
this.state.editingElement?.id === element.id &&
|
|
||||||
this.state.editingElement !== element &&
|
|
||||||
isNonDeletedElement(element)
|
|
||||||
) {
|
|
||||||
editingElement = element;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scene.replaceAllElements(actionResult.elements);
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionResult.files) {
|
|
||||||
this.files = actionResult.replaceFiles
|
|
||||||
? actionResult.files
|
|
||||||
: { ...this.files, ...actionResult.files };
|
|
||||||
this.addNewImagesToImageCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionResult.appState || editingElement || this.state.contextMenu) {
|
|
||||||
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
|
||||||
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
|
||||||
let gridSize = actionResult?.appState?.gridSize || null;
|
|
||||||
const theme =
|
|
||||||
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
|
|
||||||
const name = actionResult?.appState?.name ?? this.state.name;
|
|
||||||
const errorMessage =
|
|
||||||
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
|
|
||||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
|
||||||
viewModeEnabled = this.props.viewModeEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.props.zenModeEnabled !== "undefined") {
|
let editingElement: AppState["editingElement"] | null = null;
|
||||||
zenModeEnabled = this.props.zenModeEnabled;
|
if (actionResult.elements) {
|
||||||
}
|
actionResult.elements.forEach((element) => {
|
||||||
|
if (
|
||||||
if (typeof this.props.gridModeEnabled !== "undefined") {
|
this.state.editingElement?.id === element.id &&
|
||||||
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
|
this.state.editingElement !== element &&
|
||||||
}
|
isNonDeletedElement(element)
|
||||||
|
) {
|
||||||
editingElement =
|
editingElement = element;
|
||||||
editingElement || actionResult.appState?.editingElement || null;
|
}
|
||||||
|
|
||||||
if (editingElement?.isDeleted) {
|
|
||||||
editingElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((state) => {
|
|
||||||
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
|
||||||
// regarding the resulting type as not containing undefined
|
|
||||||
// (which the following expression will never contain)
|
|
||||||
return Object.assign(actionResult.appState || {}, {
|
|
||||||
// NOTE this will prevent opening context menu using an action
|
|
||||||
// or programmatically from the host, so it will need to be
|
|
||||||
// rewritten later
|
|
||||||
contextMenu: null,
|
|
||||||
editingElement,
|
|
||||||
viewModeEnabled,
|
|
||||||
zenModeEnabled,
|
|
||||||
gridSize,
|
|
||||||
theme,
|
|
||||||
name,
|
|
||||||
errorMessage,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
didUpdate = true;
|
if (actionResult.storeAction === StoreAction.UPDATE) {
|
||||||
}
|
this.store.shouldUpdateSnapshot();
|
||||||
|
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
|
||||||
|
this.store.shouldCaptureIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
if (!didUpdate && actionResult.storeAction !== StoreAction.NONE) {
|
this.scene.replaceAllElements(actionResult.elements);
|
||||||
this.scene.triggerUpdate();
|
}
|
||||||
}
|
|
||||||
});
|
if (actionResult.files) {
|
||||||
|
this.files = actionResult.replaceFiles
|
||||||
|
? actionResult.files
|
||||||
|
: { ...this.files, ...actionResult.files };
|
||||||
|
this.addNewImagesToImageCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionResult.appState || editingElement || this.state.contextMenu) {
|
||||||
|
if (actionResult.storeAction === StoreAction.UPDATE) {
|
||||||
|
this.store.shouldUpdateSnapshot();
|
||||||
|
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
|
||||||
|
this.store.shouldCaptureIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
||||||
|
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
||||||
|
let gridSize = actionResult?.appState?.gridSize || null;
|
||||||
|
const theme =
|
||||||
|
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
|
||||||
|
const name = actionResult?.appState?.name ?? this.state.name;
|
||||||
|
const errorMessage =
|
||||||
|
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
|
||||||
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.zenModeEnabled !== "undefined") {
|
||||||
|
zenModeEnabled = this.props.zenModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.gridModeEnabled !== "undefined") {
|
||||||
|
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editingElement =
|
||||||
|
editingElement || actionResult.appState?.editingElement || null;
|
||||||
|
|
||||||
|
if (editingElement?.isDeleted) {
|
||||||
|
editingElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((state) => {
|
||||||
|
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
||||||
|
// regarding the resulting type as not containing undefined
|
||||||
|
// (which the following expression will never contain)
|
||||||
|
return Object.assign(actionResult.appState || {}, {
|
||||||
|
// NOTE this will prevent opening context menu using an action
|
||||||
|
// or programmatically from the host, so it will need to be
|
||||||
|
// rewritten later
|
||||||
|
contextMenu: null,
|
||||||
|
editingElement,
|
||||||
|
viewModeEnabled,
|
||||||
|
zenModeEnabled,
|
||||||
|
gridSize,
|
||||||
|
theme,
|
||||||
|
name,
|
||||||
|
errorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
@@ -2282,11 +2282,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
let initialData = null;
|
let initialData = null;
|
||||||
try {
|
try {
|
||||||
if (typeof this.props.initialData === "function") {
|
initialData = (await this.props.initialData) || null;
|
||||||
initialData = (await this.props.initialData()) || null;
|
|
||||||
} else {
|
|
||||||
initialData = (await this.props.initialData) || null;
|
|
||||||
}
|
|
||||||
if (initialData?.libraryItems) {
|
if (initialData?.libraryItems) {
|
||||||
this.library
|
this.library
|
||||||
.updateLibrary({
|
.updateLibrary({
|
||||||
@@ -2489,9 +2485,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
(window as any).launchQueue?.setConsumer(() => {});
|
|
||||||
this.renderer.destroy();
|
this.renderer.destroy();
|
||||||
this.scene.destroy();
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
this.fonts = new Fonts({ scene: this.scene });
|
this.fonts = new Fonts({ scene: this.scene });
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
@@ -2500,6 +2494,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.resizeObserver?.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
|
this.scene.destroy();
|
||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
this.laserTrails.stop();
|
this.laserTrails.stop();
|
||||||
this.eraserTrail.stop();
|
this.eraserTrail.stop();
|
||||||
@@ -2570,7 +2565,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
|
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
|
||||||
addEventListener(
|
addEventListener(
|
||||||
document,
|
document,
|
||||||
EVENT.POINTER_MOVE,
|
EVENT.MOUSE_MOVE,
|
||||||
this.updateCurrentCursorPosition,
|
this.updateCurrentCursorPosition,
|
||||||
),
|
),
|
||||||
// rerender text elements on font load to fix #637 && #1553
|
// rerender text elements on font load to fix #637 && #1553
|
||||||
@@ -2811,7 +2806,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
nonDeletedElementsMap,
|
nonDeletedElementsMap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3049,31 +3044,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
if (data.text && isMaybeMermaidDefinition(data.text)) {
|
|
||||||
const api = await import("@excalidraw/mermaid-to-excalidraw");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { elements: skeletonElements, files } =
|
|
||||||
await api.parseMermaidToExcalidraw(data.text);
|
|
||||||
|
|
||||||
const elements = convertToExcalidrawElements(skeletonElements, {
|
|
||||||
regenerateIds: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addElementsFromPasteOrLibrary({
|
|
||||||
elements,
|
|
||||||
files,
|
|
||||||
position: "cursor",
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.warn(
|
|
||||||
`parsing pasted text as mermaid definition failed: ${err.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonEmptyLines = normalizeEOL(data.text)
|
const nonEmptyLines = normalizeEOL(data.text)
|
||||||
.split(/\n+/)
|
.split(/\n+/)
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@@ -3371,53 +3341,32 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
text,
|
text,
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
textAlign: DEFAULT_TEXT_ALIGN,
|
textAlign: this.state.currentItemTextAlign,
|
||||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
const fontString = getFontString({
|
|
||||||
fontSize: textElementProps.fontSize,
|
|
||||||
fontFamily: textElementProps.fontFamily,
|
|
||||||
});
|
|
||||||
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
|
||||||
const [x1, , x2] = getVisibleSceneBounds(this.state);
|
|
||||||
// long texts should not go beyond 800 pixels in width nor should it go below 200 px
|
|
||||||
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
|
|
||||||
const LINE_GAP = 10;
|
const LINE_GAP = 10;
|
||||||
let currentY = y;
|
let currentY = y;
|
||||||
|
|
||||||
const lines = isPlainPaste ? [text] : text.split("\n");
|
const lines = isPlainPaste ? [text] : text.split("\n");
|
||||||
const textElements = lines.reduce(
|
const textElements = lines.reduce(
|
||||||
(acc: ExcalidrawTextElement[], line, idx) => {
|
(acc: ExcalidrawTextElement[], line, idx) => {
|
||||||
const originalText = line.trim();
|
const text = line.trim();
|
||||||
if (originalText.length) {
|
|
||||||
|
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
||||||
|
if (text.length) {
|
||||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||||
x,
|
x,
|
||||||
y: currentY,
|
y: currentY,
|
||||||
});
|
});
|
||||||
|
|
||||||
let metrics = measureText(originalText, fontString, lineHeight);
|
|
||||||
const isTextWrapped = metrics.width > maxTextWidth;
|
|
||||||
|
|
||||||
const text = isTextWrapped
|
|
||||||
? wrapText(originalText, fontString, maxTextWidth)
|
|
||||||
: originalText;
|
|
||||||
|
|
||||||
metrics = isTextWrapped
|
|
||||||
? measureText(text, fontString, lineHeight)
|
|
||||||
: metrics;
|
|
||||||
|
|
||||||
const startX = x - metrics.width / 2;
|
|
||||||
const startY = currentY - metrics.height / 2;
|
|
||||||
|
|
||||||
const element = newTextElement({
|
const element = newTextElement({
|
||||||
...textElementProps,
|
...textElementProps,
|
||||||
x: startX,
|
x,
|
||||||
y: startY,
|
y: currentY,
|
||||||
text,
|
text,
|
||||||
originalText,
|
|
||||||
lineHeight,
|
lineHeight,
|
||||||
autoResize: !isTextWrapped,
|
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
acc.push(element);
|
acc.push(element);
|
||||||
@@ -3734,7 +3683,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements?: SceneData["elements"];
|
elements?: SceneData["elements"];
|
||||||
appState?: Pick<AppState, K> | null;
|
appState?: Pick<AppState, K> | null;
|
||||||
collaborators?: SceneData["collaborators"];
|
collaborators?: SceneData["collaborators"];
|
||||||
/** @default StoreAction.NONE */
|
/** @default StoreAction.CAPTURE */
|
||||||
storeAction?: SceneData["storeAction"];
|
storeAction?: SceneData["storeAction"];
|
||||||
}) => {
|
}) => {
|
||||||
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
|
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
|
||||||
@@ -3998,7 +3947,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
suggestedBindings: getSuggestedBindingsForArrows(
|
suggestedBindings: getSuggestedBindingsForArrows(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4169,7 +4118,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
);
|
);
|
||||||
@@ -4445,11 +4394,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
excalidrawContainer: this.excalidrawContainerRef.current,
|
excalidrawContainer: this.excalidrawContainerRef.current,
|
||||||
app: this,
|
app: this,
|
||||||
// when text is selected, it's hard (at least on iOS) to re-position the
|
|
||||||
// caret (i.e. deselect). There's not much use for always selecting
|
|
||||||
// the text on edit anyway (and users can select-all from contextmenu
|
|
||||||
// if needed)
|
|
||||||
autoSelect: !this.device.isTouchScreen,
|
|
||||||
});
|
});
|
||||||
// deselect all other elements when inserting text
|
// deselect all other elements when inserting text
|
||||||
this.deselectElements();
|
this.deselectElements();
|
||||||
@@ -4481,6 +4425,59 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the pure geometric shape of an excalidraw element
|
||||||
|
* which is then used for hit detection
|
||||||
|
*/
|
||||||
|
public getElementShape(element: ExcalidrawElement): GeometricShape {
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "diamond":
|
||||||
|
case "frame":
|
||||||
|
case "magicframe":
|
||||||
|
case "embeddable":
|
||||||
|
case "image":
|
||||||
|
case "iframe":
|
||||||
|
case "text":
|
||||||
|
case "selection":
|
||||||
|
return getPolygonShape(element);
|
||||||
|
case "arrow":
|
||||||
|
case "line": {
|
||||||
|
const roughShape =
|
||||||
|
ShapeCache.get(element)?.[0] ??
|
||||||
|
ShapeCache.generateElementShape(element, null)[0];
|
||||||
|
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldTestInside(element)
|
||||||
|
? getClosedCurveShape(
|
||||||
|
element,
|
||||||
|
roughShape,
|
||||||
|
[element.x, element.y],
|
||||||
|
element.angle,
|
||||||
|
[cx, cy],
|
||||||
|
)
|
||||||
|
: getCurveShape(roughShape, [element.x, element.y], element.angle, [
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ellipse":
|
||||||
|
return getEllipseShape(element);
|
||||||
|
|
||||||
|
case "freedraw": {
|
||||||
|
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
return getFreedrawShape(element, [cx, cy], shouldTestInside(element));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getBoundTextShape(element: ExcalidrawElement): GeometricShape | null {
|
private getBoundTextShape(element: ExcalidrawElement): GeometricShape | null {
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
element,
|
element,
|
||||||
@@ -4489,24 +4486,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return getElementShape(
|
return this.getElementShape({
|
||||||
{
|
...boundTextElement,
|
||||||
...boundTextElement,
|
// arrow's bound text accurate position is not stored in the element's property
|
||||||
// arrow's bound text accurate position is not stored in the element's property
|
// but rather calculated and returned from the following static method
|
||||||
// but rather calculated and returned from the following static method
|
...LinearElementEditor.getBoundTextElementPosition(
|
||||||
...LinearElementEditor.getBoundTextElementPosition(
|
element,
|
||||||
element,
|
boundTextElement,
|
||||||
boundTextElement,
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
),
|
||||||
),
|
});
|
||||||
},
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return getElementShape(
|
return this.getElementShape(boundTextElement);
|
||||||
boundTextElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -4545,10 +4536,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element: elementWithHighestZIndex,
|
element: elementWithHighestZIndex,
|
||||||
shape: getElementShape(
|
shape: this.getElementShape(elementWithHighestZIndex),
|
||||||
elementWithHighestZIndex,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
// when overlapping, we would like to be more precise
|
// when overlapping, we would like to be more precise
|
||||||
// this also avoids the need to update past tests
|
// this also avoids the need to update past tests
|
||||||
threshold: this.getElementHitThreshold() / 2,
|
threshold: this.getElementHitThreshold() / 2,
|
||||||
@@ -4653,7 +4641,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
shape: this.getElementShape(element),
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(element)
|
frameNameBound: isFrameLikeElement(element)
|
||||||
? this.frameNameBoundsCache.get(element)
|
? this.frameNameBoundsCache.get(element)
|
||||||
@@ -4685,10 +4673,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element: elements[index],
|
element: elements[index],
|
||||||
shape: getElementShape(
|
shape: this.getElementShape(elements[index]),
|
||||||
elements[index],
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@@ -4708,7 +4693,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
sceneY,
|
sceneY,
|
||||||
insertAtParentCenter = true,
|
insertAtParentCenter = true,
|
||||||
container,
|
container,
|
||||||
autoEdit = true,
|
|
||||||
}: {
|
}: {
|
||||||
/** X position to insert text at */
|
/** X position to insert text at */
|
||||||
sceneX: number;
|
sceneX: number;
|
||||||
@@ -4717,7 +4701,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
/** whether to attempt to insert at element center if applicable */
|
/** whether to attempt to insert at element center if applicable */
|
||||||
insertAtParentCenter?: boolean;
|
insertAtParentCenter?: boolean;
|
||||||
container?: ExcalidrawTextContainer | null;
|
container?: ExcalidrawTextContainer | null;
|
||||||
autoEdit?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
let shouldBindToContainer = false;
|
let shouldBindToContainer = false;
|
||||||
|
|
||||||
@@ -4850,16 +4833,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoEdit || existingTextElement || container) {
|
this.setState({
|
||||||
this.handleTextWysiwyg(element, {
|
editingElement: element,
|
||||||
isExistingElement: !!existingTextElement,
|
});
|
||||||
});
|
|
||||||
} else {
|
this.handleTextWysiwyg(element, {
|
||||||
this.setState({
|
isExistingElement: !!existingTextElement,
|
||||||
draggingElement: element,
|
});
|
||||||
multiElement: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleCanvasDoubleClick = (
|
private handleCanvasDoubleClick = (
|
||||||
@@ -4946,10 +4926,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: sceneX,
|
x: sceneX,
|
||||||
y: sceneY,
|
y: sceneY,
|
||||||
element: container,
|
element: container,
|
||||||
shape: getElementShape(
|
shape: this.getElementShape(container),
|
||||||
container,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@@ -5641,10 +5618,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: scenePointerX,
|
x: scenePointerX,
|
||||||
y: scenePointerY,
|
y: scenePointerY,
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(
|
shape: this.getElementShape(element),
|
||||||
element,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||||
@@ -5900,6 +5874,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (this.state.activeTool.type === "text") {
|
if (this.state.activeTool.type === "text") {
|
||||||
this.handleTextOnPointerDown(event, pointerDownState);
|
this.handleTextOnPointerDown(event, pointerDownState);
|
||||||
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
this.state.activeTool.type === "arrow" ||
|
this.state.activeTool.type === "arrow" ||
|
||||||
this.state.activeTool.type === "line"
|
this.state.activeTool.type === "line"
|
||||||
@@ -6020,7 +5995,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
const clicklength =
|
const clicklength =
|
||||||
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||||
|
|
||||||
if (this.device.editor.isMobile && clicklength < 300) {
|
if (this.device.editor.isMobile && clicklength < 300) {
|
||||||
const hitElement = this.getElementAtPosition(
|
const hitElement = this.getElementAtPosition(
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
@@ -6694,7 +6668,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
sceneY,
|
sceneY,
|
||||||
insertAtParentCenter: !event.altKey,
|
insertAtParentCenter: !event.altKey,
|
||||||
container,
|
container,
|
||||||
autoEdit: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
@@ -6763,7 +6736,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
this.scene.insertElement(element);
|
this.scene.insertElement(element);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -7025,7 +6998,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
pointerDownState.origin,
|
pointerDownState.origin,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scene.insertElement(element);
|
this.scene.insertElement(element);
|
||||||
@@ -7495,7 +7468,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
suggestedBindings: getSuggestedBindingsForArrows(
|
suggestedBindings: getSuggestedBindingsForArrows(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8016,7 +7989,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
draggingElement,
|
draggingElement,
|
||||||
this.state,
|
this.state,
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
@@ -8045,28 +8018,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(draggingElement)) {
|
|
||||||
const minWidth = getMinTextElementWidth(
|
|
||||||
getFontString({
|
|
||||||
fontSize: draggingElement.fontSize,
|
|
||||||
fontFamily: draggingElement.fontFamily,
|
|
||||||
}),
|
|
||||||
draggingElement.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (draggingElement.width < minWidth) {
|
|
||||||
mutateElement(draggingElement, {
|
|
||||||
autoResize: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetCursor();
|
|
||||||
|
|
||||||
this.handleTextWysiwyg(draggingElement, {
|
|
||||||
isExistingElement: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeTool.type !== "selection" &&
|
activeTool.type !== "selection" &&
|
||||||
draggingElement &&
|
draggingElement &&
|
||||||
@@ -8506,10 +8457,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: pointerDownState.origin.x,
|
x: pointerDownState.origin.x,
|
||||||
y: pointerDownState.origin.y,
|
y: pointerDownState.origin.y,
|
||||||
element: hitElement,
|
element: hitElement,
|
||||||
shape: getElementShape(
|
shape: this.getElementShape(hitElement),
|
||||||
hitElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(hitElement)
|
frameNameBound: isFrameLikeElement(hitElement)
|
||||||
? this.frameNameBoundsCache.get(hitElement)
|
? this.frameNameBoundsCache.get(hitElement)
|
||||||
@@ -8577,7 +8525,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
linearElements,
|
linearElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
);
|
);
|
||||||
@@ -9065,7 +9013,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}): void => {
|
}): void => {
|
||||||
const hoveredBindableElement = getHoveredElementForBinding(
|
const hoveredBindableElement = getHoveredElementForBinding(
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
suggestedBindings:
|
suggestedBindings:
|
||||||
@@ -9092,7 +9040,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
||||||
const hoveredBindableElement = getHoveredElementForBinding(
|
const hoveredBindableElement = getHoveredElementForBinding(
|
||||||
coords,
|
coords,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
hoveredBindableElement != null &&
|
hoveredBindableElement != null &&
|
||||||
@@ -9437,7 +9385,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
distance(pointerDownState.origin.y, pointerCoords.y),
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
||||||
shouldMaintainAspectRatio(event),
|
shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
this.state.zoom.value,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let [gridX, gridY] = getGridPoint(
|
let [gridX, gridY] = getGridPoint(
|
||||||
@@ -9495,7 +9442,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? !shouldMaintainAspectRatio(event)
|
? !shouldMaintainAspectRatio(event)
|
||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
this.state.zoom.value,
|
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
this.state.originSnapOffset,
|
this.state.originSnapOffset,
|
||||||
);
|
);
|
||||||
@@ -9624,7 +9570,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
const suggestedBindings = getSuggestedBindingsForArrows(
|
const suggestedBindings = getSuggestedBindingsForArrows(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
const elementsToHighlight = new Set<ExcalidrawElement>();
|
const elementsToHighlight = new Set<ExcalidrawElement>();
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("Alt+Shift+D")]}
|
shortcuts={[getShortcutKey("Alt+Shift+D")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("stats.fullTitle")}
|
label={t("stats.title")}
|
||||||
shortcuts={[getShortcutKey("Alt+/")]}
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
|
|||||||
@@ -27,99 +27,6 @@
|
|||||||
& > * {
|
& > * {
|
||||||
pointer-events: var(--ui-pointerEvents);
|
pointer-events: var(--ui-pointerEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .Stats {
|
|
||||||
width: 204px;
|
|
||||||
position: absolute;
|
|
||||||
top: 60px;
|
|
||||||
font-size: 12px;
|
|
||||||
z-index: var(--zIndex-layerUI);
|
|
||||||
pointer-events: var(--ui-pointerEvents);
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elementType {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elementsCount {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statsItem {
|
|
||||||
margin-top: 8px;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid var(--input-border-color);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
td:nth-child(2) {
|
|
||||||
min-width: 24px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
background-color: var(--default-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 12px;
|
|
||||||
right: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import { JSONExportDialog } from "./JSONExportDialog";
|
|||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
|
import { Stats } from "./Stats";
|
||||||
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
import Footer from "./footer/Footer";
|
import Footer from "./footer/Footer";
|
||||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
@@ -62,8 +64,6 @@ import Scene from "../scene/Scene";
|
|||||||
import { LaserPointerButton } from "./LaserPointerButton";
|
import { LaserPointerButton } from "./LaserPointerButton";
|
||||||
import { MagicSettings } from "./MagicSettings";
|
import { MagicSettings } from "./MagicSettings";
|
||||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||||
import { Stats } from "./Stats";
|
|
||||||
import { actionToggleStats } from "../actions";
|
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -241,11 +241,6 @@ const LayerUI = ({
|
|||||||
elements,
|
elements,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldShowStats =
|
|
||||||
appState.stats.open &&
|
|
||||||
!appState.zenModeEnabled &&
|
|
||||||
!appState.viewModeEnabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
@@ -358,15 +353,6 @@ const LayerUI = ({
|
|||||||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
||||||
<tunnels.DefaultSidebarTriggerTunnel.Out />
|
<tunnels.DefaultSidebarTriggerTunnel.Out />
|
||||||
)}
|
)}
|
||||||
{shouldShowStats && (
|
|
||||||
<Stats
|
|
||||||
scene={app.scene}
|
|
||||||
onClose={() => {
|
|
||||||
actionManager.executeAction(actionToggleStats);
|
|
||||||
}}
|
|
||||||
renderCustomStats={renderCustomStats}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
@@ -556,6 +542,17 @@ const LayerUI = ({
|
|||||||
showExitZenModeBtn={showExitZenModeBtn}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
|
{appState.showStats && (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { Section } from "./Section";
|
|||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockButton } from "./LockButton";
|
import { LockButton } from "./LockButton";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
|
import { Stats } from "./Stats";
|
||||||
|
import { actionToggleStats } from "../actions";
|
||||||
import { HandButton } from "./HandButton";
|
import { HandButton } from "./HandButton";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
@@ -155,6 +157,17 @@ export const MobileMenu = ({
|
|||||||
<>
|
<>
|
||||||
{renderSidebars()}
|
{renderSidebars()}
|
||||||
{!appState.viewModeEnabled && renderToolbar()}
|
{!appState.viewModeEnabled && renderToolbar()}
|
||||||
|
{!appState.openMenu && appState.showStats && (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@import "../css/variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Stats {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
right: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: var(--ui-pointerEvents);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 24px 8px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
float: right;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid var(--input-border-color);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td:nth-child(2) {
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
left: 12px;
|
||||||
|
right: initial;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 8px 24px;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { getCommonBounds } from "../element/bounds";
|
||||||
|
import type { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { getTargetElements } from "../scene";
|
||||||
|
import type { ExcalidrawProps, UIAppState } from "../types";
|
||||||
|
import { CloseIcon } from "./icons";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import "./Stats.scss";
|
||||||
|
|
||||||
|
export const Stats = (props: {
|
||||||
|
appState: UIAppState;
|
||||||
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
onClose: () => void;
|
||||||
|
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||||
|
}) => {
|
||||||
|
const boundingBox = getCommonBounds(props.elements);
|
||||||
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
|
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Stats">
|
||||||
|
<Island padding={2}>
|
||||||
|
<div className="close" onClick={props.onClose}>
|
||||||
|
{CloseIcon}
|
||||||
|
</div>
|
||||||
|
<h3>{t("stats.title")}</h3>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{t("stats.scene")}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.elements")}</td>
|
||||||
|
<td>{props.elements.length}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.width")}</td>
|
||||||
|
<td>{Math.round(boundingBox[2]) - Math.round(boundingBox[0])}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.height")}</td>
|
||||||
|
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{selectedElements.length === 1 && (
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{t("stats.element")}</th>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedElements.length > 1 && (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{t("stats.selected")}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.elements")}</td>
|
||||||
|
<td>{selectedElements.length}</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedElements.length > 0 && (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<td>{"x"}</td>
|
||||||
|
<td>{Math.round(selectedBoundingBox[0])}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{"y"}</td>
|
||||||
|
<td>{Math.round(selectedBoundingBox[1])}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.width")}</td>
|
||||||
|
<td>
|
||||||
|
{Math.round(
|
||||||
|
selectedBoundingBox[2] - selectedBoundingBox[0],
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.height")}</td>
|
||||||
|
<td>
|
||||||
|
{Math.round(
|
||||||
|
selectedBoundingBox[3] - selectedBoundingBox[1],
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedElements.length === 1 && (
|
||||||
|
<tr>
|
||||||
|
<td>{t("stats.angle")}</td>
|
||||||
|
<td>
|
||||||
|
{`${Math.round(
|
||||||
|
(selectedElements[0].angle * 180) / Math.PI,
|
||||||
|
)}°`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{props.renderCustomStats?.(props.elements, props.appState)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Island>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
|
||||||
import { angleIcon } from "../icons";
|
|
||||||
import DragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface AngleProps {
|
|
||||||
element: ExcalidrawElement;
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
property: "angle";
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 15;
|
|
||||||
|
|
||||||
const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const origElement = originalElements[0];
|
|
||||||
if (origElement) {
|
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
|
||||||
if (!latestElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
|
||||||
mutateElement(latestElement, {
|
|
||||||
angle: nextAngle,
|
|
||||||
});
|
|
||||||
updateBindings(latestElement, elementsMap);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalAngleInDegrees =
|
|
||||||
Math.round(radianToDegree(origElement.angle) * 100) / 100;
|
|
||||||
const changeInDegrees = Math.round(accumulatedChange);
|
|
||||||
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextAngleInDegrees =
|
|
||||||
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
|
||||||
|
|
||||||
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
|
||||||
|
|
||||||
mutateElement(latestElement, {
|
|
||||||
angle: nextAngle,
|
|
||||||
});
|
|
||||||
updateBindings(latestElement, elementsMap);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
|
||||||
return (
|
|
||||||
<DragInput
|
|
||||||
label="A"
|
|
||||||
icon={angleIcon}
|
|
||||||
value={Math.round((radianToDegree(element.angle) % 360) * 100) / 100}
|
|
||||||
elements={[element]}
|
|
||||||
dragInputCallback={handleDegreeChange}
|
|
||||||
editable={isPropertyEditable(element, "angle")}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
property={property}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Angle;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { InlineIcon } from "../InlineIcon";
|
|
||||||
import { collapseDownIcon, collapseUpIcon } from "../icons";
|
|
||||||
|
|
||||||
interface CollapsibleProps {
|
|
||||||
label: React.ReactNode;
|
|
||||||
// having it controlled so that the state is managed outside
|
|
||||||
// this is to keep the user's previous choice even when the
|
|
||||||
// Collapsible is unmounted
|
|
||||||
open: boolean;
|
|
||||||
openTrigger: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Collapsible = ({
|
|
||||||
label,
|
|
||||||
open,
|
|
||||||
openTrigger,
|
|
||||||
children,
|
|
||||||
}: CollapsibleProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
onClick={openTrigger}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
<InlineIcon icon={open ? collapseUpIcon : collapseDownIcon} />
|
|
||||||
</div>
|
|
||||||
{open && <>{children}</>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Collapsible;
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import type { ExcalidrawElement } from "../../element/types";
|
|
||||||
import DragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getStepSizedValue, isPropertyEditable, resizeElement } from "./utils";
|
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface DimensionDragInputProps {
|
|
||||||
property: "width" | "height";
|
|
||||||
element: ExcalidrawElement;
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
|
|
||||||
return element.type === "image";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDimensionChange: DragInputCallbackType<
|
|
||||||
DimensionDragInputProps["property"]
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
|
||||||
shouldKeepAspectRatio,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const origElement = originalElements[0];
|
|
||||||
if (origElement) {
|
|
||||||
const keepAspectRatio =
|
|
||||||
shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement);
|
|
||||||
const aspectRatio = origElement.width / origElement.height;
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
const nextWidth = Math.max(
|
|
||||||
property === "width"
|
|
||||||
? nextValue
|
|
||||||
: keepAspectRatio
|
|
||||||
? nextValue * aspectRatio
|
|
||||||
: origElement.width,
|
|
||||||
MIN_WIDTH_OR_HEIGHT,
|
|
||||||
);
|
|
||||||
const nextHeight = Math.max(
|
|
||||||
property === "height"
|
|
||||||
? nextValue
|
|
||||||
: keepAspectRatio
|
|
||||||
? nextValue / aspectRatio
|
|
||||||
: origElement.height,
|
|
||||||
MIN_WIDTH_OR_HEIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
resizeElement(
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
keepAspectRatio,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
|
||||||
const changeInHeight = property === "height" ? accumulatedChange : 0;
|
|
||||||
|
|
||||||
let nextWidth = Math.max(0, origElement.width + changeInWidth);
|
|
||||||
if (property === "width") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextHeight = Math.max(0, origElement.height + changeInHeight);
|
|
||||||
if (property === "height") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextHeight = Math.round(nextHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keepAspectRatio) {
|
|
||||||
if (property === "width") {
|
|
||||||
nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100;
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
|
||||||
|
|
||||||
resizeElement(
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
keepAspectRatio,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const DimensionDragInput = ({
|
|
||||||
property,
|
|
||||||
element,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
}: DimensionDragInputProps) => {
|
|
||||||
const value =
|
|
||||||
Math.round((property === "width" ? element.width : element.height) * 100) /
|
|
||||||
100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragInput
|
|
||||||
label={property === "width" ? "W" : "H"}
|
|
||||||
elements={[element]}
|
|
||||||
dragInputCallback={handleDimensionChange}
|
|
||||||
value={value}
|
|
||||||
editable={isPropertyEditable(element, property)}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
property={property}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DimensionDragInput;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
.excalidraw {
|
|
||||||
.drag-input-container {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-input-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
border: 1px solid var(--default-border-color);
|
|
||||||
border-right: 0;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--popup-text-color);
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
|
||||||
border-right: 1px solid var(--default-border-color);
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-input {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
border: 0;
|
|
||||||
outline: none;
|
|
||||||
height: 2rem;
|
|
||||||
border: 1px solid var(--default-border-color);
|
|
||||||
border-left: 0;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
|
|
||||||
border-left: 1px solid var(--default-border-color);
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
padding: 0.5rem;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
appearance: none;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { EVENT } from "../../constants";
|
|
||||||
import { KEYS } from "../../keys";
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
|
||||||
import { deepCopyElement } from "../../element/newElement";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { useApp } from "../App";
|
|
||||||
import { InlineIcon } from "../InlineIcon";
|
|
||||||
import type { StatsInputProperty } from "./utils";
|
|
||||||
import { SMALLEST_DELTA } from "./utils";
|
|
||||||
import { StoreAction } from "../../store";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
|
|
||||||
import "./DragInput.scss";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
import { cloneJSON } from "../../utils";
|
|
||||||
|
|
||||||
export type DragInputCallbackType<
|
|
||||||
P extends StatsInputProperty,
|
|
||||||
E = ExcalidrawElement,
|
|
||||||
> = (props: {
|
|
||||||
accumulatedChange: number;
|
|
||||||
instantChange: number;
|
|
||||||
originalElements: readonly E[];
|
|
||||||
originalElementsMap: ElementsMap;
|
|
||||||
shouldKeepAspectRatio: boolean;
|
|
||||||
shouldChangeByStepSize: boolean;
|
|
||||||
nextValue?: number;
|
|
||||||
property: P;
|
|
||||||
scene: Scene;
|
|
||||||
originalAppState: AppState;
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
interface StatsDragInputProps<
|
|
||||||
T extends StatsInputProperty,
|
|
||||||
E = ExcalidrawElement,
|
|
||||||
> {
|
|
||||||
label: string | React.ReactNode;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
value: number | "Mixed";
|
|
||||||
elements: readonly E[];
|
|
||||||
editable?: boolean;
|
|
||||||
shouldKeepAspectRatio?: boolean;
|
|
||||||
dragInputCallback: DragInputCallbackType<T, E>;
|
|
||||||
property: T;
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatsDragInput = <
|
|
||||||
T extends StatsInputProperty,
|
|
||||||
E extends ExcalidrawElement = ExcalidrawElement,
|
|
||||||
>({
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
dragInputCallback,
|
|
||||||
value,
|
|
||||||
elements,
|
|
||||||
editable = true,
|
|
||||||
shouldKeepAspectRatio,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
}: StatsDragInputProps<T, E>) => {
|
|
||||||
const app = useApp();
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const labelRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState(value.toString());
|
|
||||||
|
|
||||||
const stateRef = useRef<{
|
|
||||||
originalAppState: AppState;
|
|
||||||
originalElements: readonly E[];
|
|
||||||
lastUpdatedValue: string;
|
|
||||||
updatePending: boolean;
|
|
||||||
}>(null!);
|
|
||||||
if (!stateRef.current) {
|
|
||||||
stateRef.current = {
|
|
||||||
originalAppState: cloneJSON(appState),
|
|
||||||
originalElements: elements,
|
|
||||||
lastUpdatedValue: inputValue,
|
|
||||||
updatePending: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const inputValue = value.toString();
|
|
||||||
setInputValue(inputValue);
|
|
||||||
stateRef.current.lastUpdatedValue = inputValue;
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const handleInputValue = (
|
|
||||||
updatedValue: string,
|
|
||||||
elements: readonly E[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
if (!stateRef.current.updatePending) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
stateRef.current.updatePending = false;
|
|
||||||
|
|
||||||
const parsed = Number(updatedValue);
|
|
||||||
if (isNaN(parsed)) {
|
|
||||||
setInputValue(value.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rounded = Number(parsed.toFixed(2));
|
|
||||||
const original = Number(value);
|
|
||||||
|
|
||||||
// only update when
|
|
||||||
// 1. original was "Mixed" and we have a new value
|
|
||||||
// 2. original was not "Mixed" and the difference between a new value and previous value is greater
|
|
||||||
// than the smallest delta allowed, which is 0.01
|
|
||||||
// reason: idempotent to avoid unnecessary
|
|
||||||
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
|
||||||
stateRef.current.lastUpdatedValue = updatedValue;
|
|
||||||
dragInputCallback({
|
|
||||||
accumulatedChange: 0,
|
|
||||||
instantChange: 0,
|
|
||||||
originalElements: elements,
|
|
||||||
originalElementsMap: app.scene.getNonDeletedElementsMap(),
|
|
||||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
|
||||||
shouldChangeByStepSize: false,
|
|
||||||
nextValue: rounded,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
originalAppState: appState,
|
|
||||||
});
|
|
||||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputValueRef = useRef(handleInputValue);
|
|
||||||
handleInputValueRef.current = handleInputValue;
|
|
||||||
|
|
||||||
// make sure that clicking on canvas (which umounts the component)
|
|
||||||
// updates current input value (blur isn't triggered)
|
|
||||||
useEffect(() => {
|
|
||||||
const input = inputRef.current;
|
|
||||||
return () => {
|
|
||||||
const nextValue = input?.value;
|
|
||||||
if (nextValue) {
|
|
||||||
handleInputValueRef.current(
|
|
||||||
nextValue,
|
|
||||||
stateRef.current.originalElements,
|
|
||||||
stateRef.current.originalAppState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
// we need to track change of `editable` state as mount/unmount
|
|
||||||
// because react doesn't trigger `blur` when a an input is blurred due
|
|
||||||
// to being disabled (https://github.com/facebook/react/issues/9142).
|
|
||||||
// As such, if we keep rendering disabled inputs, then change in selection
|
|
||||||
// to an element that has a given property as non-editable would not trigger
|
|
||||||
// blur/unmount and wouldn't update the value.
|
|
||||||
editable,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!editable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx("drag-input-container", !editable && "disabled")}
|
|
||||||
data-testid={label}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="drag-input-label"
|
|
||||||
ref={labelRef}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
if (inputRef.current && editable) {
|
|
||||||
let startValue = Number(inputRef.current.value);
|
|
||||||
if (isNaN(startValue)) {
|
|
||||||
startValue = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastPointer: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
|
||||||
app.scene
|
|
||||||
.getNonDeletedElements()
|
|
||||||
.reduce((acc: ElementsMap, element) => {
|
|
||||||
acc.set(element.id, deepCopyElement(element));
|
|
||||||
return acc;
|
|
||||||
}, new Map());
|
|
||||||
|
|
||||||
let originalElements: readonly E[] | null = elements.map(
|
|
||||||
(element) => originalElementsMap!.get(element.id) as E,
|
|
||||||
);
|
|
||||||
|
|
||||||
const originalAppState: AppState = cloneJSON(appState);
|
|
||||||
|
|
||||||
let accumulatedChange: number | null = null;
|
|
||||||
|
|
||||||
document.body.classList.add("excalidraw-cursor-resize");
|
|
||||||
|
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
|
||||||
if (!accumulatedChange) {
|
|
||||||
accumulatedChange = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
lastPointer &&
|
|
||||||
originalElementsMap !== null &&
|
|
||||||
originalElements !== null &&
|
|
||||||
accumulatedChange !== null
|
|
||||||
) {
|
|
||||||
const instantChange = event.clientX - lastPointer.x;
|
|
||||||
accumulatedChange += instantChange;
|
|
||||||
|
|
||||||
dragInputCallback({
|
|
||||||
accumulatedChange,
|
|
||||||
instantChange,
|
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
|
||||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
|
||||||
shouldChangeByStepSize: event.shiftKey,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
originalAppState,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPointer = {
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, false);
|
|
||||||
window.addEventListener(
|
|
||||||
EVENT.POINTER_UP,
|
|
||||||
() => {
|
|
||||||
window.removeEventListener(
|
|
||||||
EVENT.POINTER_MOVE,
|
|
||||||
onPointerMove,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
|
||||||
|
|
||||||
lastPointer = null;
|
|
||||||
accumulatedChange = null;
|
|
||||||
originalElements = null;
|
|
||||||
originalElementsMap = null;
|
|
||||||
|
|
||||||
document.body.classList.remove("excalidraw-cursor-resize");
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onPointerEnter={() => {
|
|
||||||
if (labelRef.current) {
|
|
||||||
labelRef.current.style.cursor = "ew-resize";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon ? <InlineIcon icon={icon} /> : label}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="drag-input"
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck="false"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (editable) {
|
|
||||||
const eventTarget = event.target;
|
|
||||||
if (
|
|
||||||
eventTarget instanceof HTMLInputElement &&
|
|
||||||
event.key === KEYS.ENTER
|
|
||||||
) {
|
|
||||||
handleInputValue(eventTarget.value, elements, appState);
|
|
||||||
app.focusContainer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={inputRef}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(event) => {
|
|
||||||
stateRef.current.updatePending = true;
|
|
||||||
setInputValue(event.target.value);
|
|
||||||
}}
|
|
||||||
onFocus={(event) => {
|
|
||||||
event.target.select();
|
|
||||||
stateRef.current.originalElements = elements;
|
|
||||||
stateRef.current.originalAppState = cloneJSON(appState);
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
if (!inputValue) {
|
|
||||||
setInputValue(value.toString());
|
|
||||||
} else if (editable) {
|
|
||||||
handleInputValue(
|
|
||||||
event.target.value,
|
|
||||||
stateRef.current.originalElements,
|
|
||||||
stateRef.current.originalAppState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!editable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatsDragInput;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
} from "../../element/types";
|
|
||||||
import StatsDragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import { getStepSizedValue } from "./utils";
|
|
||||||
import { fontSizeIcon } from "../icons";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
import { isTextElement, redrawTextBoundingBox } from "../../element";
|
|
||||||
import { hasBoundTextElement } from "../../element/typeChecks";
|
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
|
||||||
|
|
||||||
interface FontSizeProps {
|
|
||||||
element: ExcalidrawElement;
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
property: "fontSize";
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_FONT_SIZE = 4;
|
|
||||||
const STEP_SIZE = 4;
|
|
||||||
|
|
||||||
const handleFontSizeChange: DragInputCallbackType<
|
|
||||||
FontSizeProps["property"],
|
|
||||||
ExcalidrawTextElement
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
const origElement = originalElements[0];
|
|
||||||
if (origElement) {
|
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
|
||||||
if (!latestElement || !isTextElement(latestElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextFontSize;
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
|
||||||
} else if (origElement.type === "text") {
|
|
||||||
const originalFontSize = Math.round(origElement.fontSize);
|
|
||||||
const changeInFontSize = Math.round(accumulatedChange);
|
|
||||||
nextFontSize = Math.max(
|
|
||||||
originalFontSize + changeInFontSize,
|
|
||||||
MIN_FONT_SIZE,
|
|
||||||
);
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextFontSize) {
|
|
||||||
mutateElement(latestElement, {
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
});
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
latestElement,
|
|
||||||
scene.getContainerElement(latestElement),
|
|
||||||
scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const FontSize = ({ element, scene, appState, property }: FontSizeProps) => {
|
|
||||||
const _element = isTextElement(element)
|
|
||||||
? element
|
|
||||||
: hasBoundTextElement(element)
|
|
||||||
? getBoundTextElement(element, scene.getNonDeletedElementsMap())
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!_element) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatsDragInput
|
|
||||||
label="F"
|
|
||||||
value={Math.round(_element.fontSize * 10) / 10}
|
|
||||||
elements={[_element]}
|
|
||||||
dragInputCallback={handleFontSizeChange}
|
|
||||||
icon={fontSizeIcon}
|
|
||||||
appState={appState}
|
|
||||||
scene={scene}
|
|
||||||
property={property}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FontSize;
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
|
||||||
import { isInGroup } from "../../groups";
|
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import { angleIcon } from "../icons";
|
|
||||||
import DragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface MultiAngleProps {
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
property: "angle";
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 15;
|
|
||||||
|
|
||||||
const handleDegreeChange: DragInputCallbackType<
|
|
||||||
MultiAngleProps["property"]
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const editableLatestIndividualElements = originalElements
|
|
||||||
.map((el) => elementsMap.get(el.id))
|
|
||||||
.filter((el) => el && !isInGroup(el) && isPropertyEditable(el, property));
|
|
||||||
const editableOriginalIndividualElements = originalElements.filter(
|
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, property),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
|
||||||
|
|
||||||
for (const element of editableLatestIndividualElements) {
|
|
||||||
if (!element) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
mutateElement(
|
|
||||||
element,
|
|
||||||
{
|
|
||||||
angle: nextAngle,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
|
||||||
if (boundTextElement && !isArrowElement(element)) {
|
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < editableLatestIndividualElements.length; i++) {
|
|
||||||
const latestElement = editableLatestIndividualElements[i];
|
|
||||||
if (!latestElement) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const originalElement = editableOriginalIndividualElements[i];
|
|
||||||
const originalAngleInDegrees =
|
|
||||||
Math.round(radianToDegree(originalElement.angle) * 100) / 100;
|
|
||||||
const changeInDegrees = Math.round(accumulatedChange);
|
|
||||||
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextAngleInDegrees =
|
|
||||||
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
|
|
||||||
|
|
||||||
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
angle: nextAngle,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scene.triggerUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const MultiAngle = ({
|
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
property,
|
|
||||||
}: MultiAngleProps) => {
|
|
||||||
const editableLatestIndividualElements = elements.filter(
|
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
|
||||||
);
|
|
||||||
const angles = editableLatestIndividualElements.map(
|
|
||||||
(el) => Math.round((radianToDegree(el.angle) % 360) * 100) / 100,
|
|
||||||
);
|
|
||||||
const value = new Set(angles).size === 1 ? angles[0] : "Mixed";
|
|
||||||
|
|
||||||
const editable = editableLatestIndividualElements.some((el) =>
|
|
||||||
isPropertyEditable(el, "angle"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragInput
|
|
||||||
label="A"
|
|
||||||
icon={angleIcon}
|
|
||||||
value={value}
|
|
||||||
elements={elements}
|
|
||||||
dragInputCallback={handleDegreeChange}
|
|
||||||
editable={editable}
|
|
||||||
appState={appState}
|
|
||||||
scene={scene}
|
|
||||||
property={property}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiAngle;
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { getCommonBounds, isTextElement } from "../../element";
|
|
||||||
import { updateBoundElements } from "../../element/binding";
|
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import { rescalePointsInElement } from "../../element/resizeElements";
|
|
||||||
import {
|
|
||||||
getBoundTextElement,
|
|
||||||
handleBindTextResize,
|
|
||||||
} from "../../element/textElement";
|
|
||||||
import type {
|
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../../element/types";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState, Point } from "../../types";
|
|
||||||
import DragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
|
||||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
|
||||||
import type { AtomicUnit } from "./utils";
|
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
|
||||||
|
|
||||||
interface MultiDimensionProps {
|
|
||||||
property: "width" | "height";
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
elementsMap: NonDeletedSceneElementsMap;
|
|
||||||
atomicUnits: AtomicUnit[];
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
|
|
||||||
const getResizedUpdates = (
|
|
||||||
anchorX: number,
|
|
||||||
anchorY: number,
|
|
||||||
scale: number,
|
|
||||||
origElement: ExcalidrawElement,
|
|
||||||
) => {
|
|
||||||
const offsetX = origElement.x - anchorX;
|
|
||||||
const offsetY = origElement.y - anchorY;
|
|
||||||
const nextWidth = origElement.width * scale;
|
|
||||||
const nextHeight = origElement.height * scale;
|
|
||||||
const x = anchorX + offsetX * scale;
|
|
||||||
const y = anchorY + offsetY * scale;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
...rescalePointsInElement(origElement, nextWidth, nextHeight, false),
|
|
||||||
...(isTextElement(origElement)
|
|
||||||
? { fontSize: origElement.fontSize * scale }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeElementInGroup = (
|
|
||||||
anchorX: number,
|
|
||||||
anchorY: number,
|
|
||||||
property: MultiDimensionProps["property"],
|
|
||||||
scale: number,
|
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
origElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
|
||||||
|
|
||||||
mutateElement(latestElement, updates, false);
|
|
||||||
const boundTextElement = getBoundTextElement(
|
|
||||||
origElement,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
if (boundTextElement) {
|
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
|
||||||
updateBoundElements(latestElement, elementsMap, {
|
|
||||||
newSize: { width: updates.width, height: updates.height },
|
|
||||||
});
|
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
|
||||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
|
||||||
mutateElement(
|
|
||||||
latestBoundTextElement,
|
|
||||||
{
|
|
||||||
fontSize: newFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
handleBindTextResize(
|
|
||||||
latestElement,
|
|
||||||
elementsMap,
|
|
||||||
property === "width" ? "e" : "s",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeGroup = (
|
|
||||||
nextWidth: number,
|
|
||||||
nextHeight: number,
|
|
||||||
initialHeight: number,
|
|
||||||
aspectRatio: number,
|
|
||||||
anchor: Point,
|
|
||||||
property: MultiDimensionProps["property"],
|
|
||||||
latestElements: ExcalidrawElement[],
|
|
||||||
originalElements: ExcalidrawElement[],
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
// keep aspect ratio for groups
|
|
||||||
if (property === "width") {
|
|
||||||
nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100;
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scale = nextHeight / initialHeight;
|
|
||||||
|
|
||||||
for (let i = 0; i < originalElements.length; i++) {
|
|
||||||
const origElement = originalElements[i];
|
|
||||||
const latestElement = latestElements[i];
|
|
||||||
|
|
||||||
resizeElementInGroup(
|
|
||||||
anchor[0],
|
|
||||||
anchor[1],
|
|
||||||
property,
|
|
||||||
scale,
|
|
||||||
latestElement,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDimensionChange: DragInputCallbackType<
|
|
||||||
MultiDimensionProps["property"]
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
|
||||||
originalAppState,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
scene,
|
|
||||||
property,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
for (const atomicUnit of atomicUnits) {
|
|
||||||
const elementsInUnit = getElementsInAtomicUnit(
|
|
||||||
atomicUnit,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const latestElements = elementsInUnit.map((el) => el.latest!);
|
|
||||||
const originalElements = elementsInUnit.map((el) => el.original!);
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(originalElements);
|
|
||||||
const initialWidth = x2 - x1;
|
|
||||||
const initialHeight = y2 - y1;
|
|
||||||
const aspectRatio = initialWidth / initialHeight;
|
|
||||||
const nextWidth = Math.max(
|
|
||||||
MIN_WIDTH_OR_HEIGHT,
|
|
||||||
property === "width" ? Math.max(0, nextValue) : initialWidth,
|
|
||||||
);
|
|
||||||
const nextHeight = Math.max(
|
|
||||||
MIN_WIDTH_OR_HEIGHT,
|
|
||||||
property === "height" ? Math.max(0, nextValue) : initialHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
resizeGroup(
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
initialHeight,
|
|
||||||
aspectRatio,
|
|
||||||
[x1, y1],
|
|
||||||
property,
|
|
||||||
latestElements,
|
|
||||||
originalElements,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
const latestElement = el?.latest;
|
|
||||||
const origElement = el?.original;
|
|
||||||
|
|
||||||
if (
|
|
||||||
latestElement &&
|
|
||||||
origElement &&
|
|
||||||
isPropertyEditable(latestElement, property)
|
|
||||||
) {
|
|
||||||
let nextWidth =
|
|
||||||
property === "width" ? Math.max(0, nextValue) : latestElement.width;
|
|
||||||
if (property === "width") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextHeight =
|
|
||||||
property === "height"
|
|
||||||
? Math.max(0, nextValue)
|
|
||||||
: latestElement.height;
|
|
||||||
if (property === "height") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextHeight = Math.round(nextHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
|
||||||
|
|
||||||
resizeElement(
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
false,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
|
||||||
const changeInHeight = property === "height" ? accumulatedChange : 0;
|
|
||||||
|
|
||||||
for (const atomicUnit of atomicUnits) {
|
|
||||||
const elementsInUnit = getElementsInAtomicUnit(
|
|
||||||
atomicUnit,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const latestElements = elementsInUnit.map((el) => el.latest!);
|
|
||||||
const originalElements = elementsInUnit.map((el) => el.original!);
|
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(originalElements);
|
|
||||||
const initialWidth = x2 - x1;
|
|
||||||
const initialHeight = y2 - y1;
|
|
||||||
const aspectRatio = initialWidth / initialHeight;
|
|
||||||
let nextWidth = Math.max(0, initialWidth + changeInWidth);
|
|
||||||
if (property === "width") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextHeight = Math.max(0, initialHeight + changeInHeight);
|
|
||||||
if (property === "height") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextHeight = Math.round(nextHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
|
||||||
|
|
||||||
resizeGroup(
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
initialHeight,
|
|
||||||
aspectRatio,
|
|
||||||
[x1, y1],
|
|
||||||
property,
|
|
||||||
latestElements,
|
|
||||||
originalElements,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
const latestElement = el?.latest;
|
|
||||||
const origElement = el?.original;
|
|
||||||
|
|
||||||
if (
|
|
||||||
latestElement &&
|
|
||||||
origElement &&
|
|
||||||
isPropertyEditable(latestElement, property)
|
|
||||||
) {
|
|
||||||
let nextWidth = Math.max(0, origElement.width + changeInWidth);
|
|
||||||
if (property === "width") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextWidth = Math.round(nextWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextHeight = Math.max(0, origElement.height + changeInHeight);
|
|
||||||
if (property === "height") {
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
|
||||||
} else {
|
|
||||||
nextHeight = Math.round(nextHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
|
||||||
|
|
||||||
resizeElement(nextWidth, nextHeight, false, origElement, elementsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const MultiDimension = ({
|
|
||||||
property,
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
atomicUnits,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
}: MultiDimensionProps) => {
|
|
||||||
const sizes = useMemo(
|
|
||||||
() =>
|
|
||||||
atomicUnits.map((atomicUnit) => {
|
|
||||||
const elementsInUnit = getElementsInAtomicUnit(atomicUnit, elementsMap);
|
|
||||||
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(
|
|
||||||
elementsInUnit.map((el) => el.latest),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
Math.round((property === "width" ? x2 - x1 : y2 - y1) * 100) / 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
|
|
||||||
return (
|
|
||||||
Math.round(
|
|
||||||
(property === "width" ? el.latest.width : el.latest.height) * 100,
|
|
||||||
) / 100
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[elementsMap, atomicUnits, property],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value =
|
|
||||||
new Set(sizes).size === 1 ? Math.round(sizes[0] * 100) / 100 : "Mixed";
|
|
||||||
|
|
||||||
const editable = sizes.length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragInput
|
|
||||||
label={property === "width" ? "W" : "H"}
|
|
||||||
elements={elements}
|
|
||||||
dragInputCallback={handleDimensionChange}
|
|
||||||
value={value}
|
|
||||||
editable={editable}
|
|
||||||
appState={appState}
|
|
||||||
property={property}
|
|
||||||
scene={scene}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiDimension;
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { isTextElement, redrawTextBoundingBox } from "../../element";
|
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import { hasBoundTextElement } from "../../element/typeChecks";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../../element/types";
|
|
||||||
import { isInGroup } from "../../groups";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import { fontSizeIcon } from "../icons";
|
|
||||||
import StatsDragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getStepSizedValue } from "./utils";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
|
||||||
|
|
||||||
interface MultiFontSizeProps {
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
scene: Scene;
|
|
||||||
elementsMap: NonDeletedSceneElementsMap;
|
|
||||||
appState: AppState;
|
|
||||||
property: "fontSize";
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_FONT_SIZE = 4;
|
|
||||||
const STEP_SIZE = 4;
|
|
||||||
|
|
||||||
const getApplicableTextElements = (
|
|
||||||
elements: readonly (ExcalidrawElement | undefined)[],
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
) =>
|
|
||||||
elements.reduce(
|
|
||||||
(acc: ExcalidrawTextElement[], el) => {
|
|
||||||
if (!el || isInGroup(el)) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
if (isTextElement(el)) {
|
|
||||||
acc.push(el);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
if (hasBoundTextElement(el)) {
|
|
||||||
const boundTextElement = getBoundTextElement(el, elementsMap);
|
|
||||||
if (boundTextElement) {
|
|
||||||
acc.push(boundTextElement);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFontSizeChange: DragInputCallbackType<
|
|
||||||
MultiFontSizeProps["property"],
|
|
||||||
ExcalidrawTextElement
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const latestTextElements = originalElements.map((el) =>
|
|
||||||
elementsMap.get(el.id),
|
|
||||||
) as ExcalidrawTextElement[];
|
|
||||||
|
|
||||||
let nextFontSize;
|
|
||||||
|
|
||||||
if (nextValue) {
|
|
||||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
|
||||||
|
|
||||||
for (const textElement of latestTextElements) {
|
|
||||||
mutateElement(
|
|
||||||
textElement,
|
|
||||||
{
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
textElement,
|
|
||||||
scene.getContainerElement(textElement),
|
|
||||||
elementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
} else {
|
|
||||||
const originalTextElements = originalElements as ExcalidrawTextElement[];
|
|
||||||
|
|
||||||
for (let i = 0; i < latestTextElements.length; i++) {
|
|
||||||
const latestElement = latestTextElements[i];
|
|
||||||
const originalElement = originalTextElements[i];
|
|
||||||
|
|
||||||
const originalFontSize = Math.round(originalElement.fontSize);
|
|
||||||
const changeInFontSize = Math.round(accumulatedChange);
|
|
||||||
let nextFontSize = Math.max(
|
|
||||||
originalFontSize + changeInFontSize,
|
|
||||||
MIN_FONT_SIZE,
|
|
||||||
);
|
|
||||||
if (shouldChangeByStepSize) {
|
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
|
||||||
}
|
|
||||||
mutateElement(
|
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
latestElement,
|
|
||||||
scene.getContainerElement(latestElement),
|
|
||||||
elementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MultiFontSize = ({
|
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
property,
|
|
||||||
elementsMap,
|
|
||||||
}: MultiFontSizeProps) => {
|
|
||||||
const latestTextElements = getApplicableTextElements(elements, elementsMap);
|
|
||||||
|
|
||||||
if (!latestTextElements.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontSizes = latestTextElements.map(
|
|
||||||
(textEl) => Math.round(textEl.fontSize * 10) / 10,
|
|
||||||
);
|
|
||||||
const value = new Set(fontSizes).size === 1 ? fontSizes[0] : "Mixed";
|
|
||||||
const editable = fontSizes.length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatsDragInput
|
|
||||||
label="F"
|
|
||||||
icon={fontSizeIcon}
|
|
||||||
elements={latestTextElements}
|
|
||||||
dragInputCallback={handleFontSizeChange}
|
|
||||||
value={value}
|
|
||||||
editable={editable}
|
|
||||||
scene={scene}
|
|
||||||
property={property}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiFontSize;
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
import type {
|
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../../element/types";
|
|
||||||
import { rotate } from "../../math";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import StatsDragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
|
||||||
import { getCommonBounds, isTextElement } from "../../element";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
|
||||||
import type { AtomicUnit } from "./utils";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface MultiPositionProps {
|
|
||||||
property: "x" | "y";
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
elementsMap: ElementsMap;
|
|
||||||
atomicUnits: AtomicUnit[];
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
|
|
||||||
const moveElements = (
|
|
||||||
property: MultiPositionProps["property"],
|
|
||||||
changeInTopX: number,
|
|
||||||
changeInTopY: number,
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
originalElements: readonly ExcalidrawElement[],
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
|
||||||
const origElement = originalElements[i];
|
|
||||||
|
|
||||||
const [cx, cy] = [
|
|
||||||
origElement.x + origElement.width / 2,
|
|
||||||
origElement.y + origElement.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
origElement.x,
|
|
||||||
origElement.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTopLeftX =
|
|
||||||
property === "x" ? Math.round(topLeftX + changeInTopX) : topLeftX;
|
|
||||||
|
|
||||||
const newTopLeftY =
|
|
||||||
property === "y" ? Math.round(topLeftY + changeInTopY) : topLeftY;
|
|
||||||
|
|
||||||
moveElement(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveGroupTo = (
|
|
||||||
nextX: number,
|
|
||||||
nextY: number,
|
|
||||||
originalElements: ExcalidrawElement[],
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
scene: Scene,
|
|
||||||
) => {
|
|
||||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
|
||||||
const offsetX = nextX - x1;
|
|
||||||
const offsetY = nextY - y1;
|
|
||||||
|
|
||||||
for (let i = 0; i < originalElements.length; i++) {
|
|
||||||
const origElement = originalElements[i];
|
|
||||||
|
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
|
||||||
if (!latestElement) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bound texts are moved with their containers
|
|
||||||
if (!isTextElement(latestElement) || !latestElement.containerId) {
|
|
||||||
const [cx, cy] = [
|
|
||||||
latestElement.x + latestElement.width / 2,
|
|
||||||
latestElement.y + latestElement.height / 2,
|
|
||||||
];
|
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
latestElement.x,
|
|
||||||
latestElement.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
latestElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
moveElement(
|
|
||||||
topLeftX + offsetX,
|
|
||||||
topLeftY + offsetY,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType<
|
|
||||||
MultiPositionProps["property"]
|
|
||||||
> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
originalAppState,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
for (const atomicUnit of getAtomicUnits(
|
|
||||||
originalElements,
|
|
||||||
originalAppState,
|
|
||||||
)) {
|
|
||||||
const elementsInUnit = getElementsInAtomicUnit(
|
|
||||||
atomicUnit,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const [x1, y1, ,] = getCommonBounds(
|
|
||||||
elementsInUnit.map((el) => el.latest!),
|
|
||||||
);
|
|
||||||
const newTopLeftX = property === "x" ? nextValue : x1;
|
|
||||||
const newTopLeftY = property === "y" ? nextValue : y1;
|
|
||||||
|
|
||||||
moveGroupTo(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
elementsInUnit.map((el) => el.original),
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
scene,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const origElement = elementsInUnit[0]?.original;
|
|
||||||
const latestElement = elementsInUnit[0]?.latest;
|
|
||||||
if (
|
|
||||||
origElement &&
|
|
||||||
latestElement &&
|
|
||||||
isPropertyEditable(latestElement, property)
|
|
||||||
) {
|
|
||||||
const [cx, cy] = [
|
|
||||||
origElement.x + origElement.width / 2,
|
|
||||||
origElement.y + origElement.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
origElement.x,
|
|
||||||
origElement.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTopLeftX = property === "x" ? nextValue : topLeftX;
|
|
||||||
const newTopLeftY = property === "y" ? nextValue : topLeftY;
|
|
||||||
moveElement(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const change = shouldChangeByStepSize
|
|
||||||
? getStepSizedValue(accumulatedChange, STEP_SIZE)
|
|
||||||
: accumulatedChange;
|
|
||||||
|
|
||||||
const changeInTopX = property === "x" ? change : 0;
|
|
||||||
const changeInTopY = property === "y" ? change : 0;
|
|
||||||
|
|
||||||
moveElements(
|
|
||||||
property,
|
|
||||||
changeInTopX,
|
|
||||||
changeInTopY,
|
|
||||||
originalElements,
|
|
||||||
originalElements,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
scene.triggerUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const MultiPosition = ({
|
|
||||||
property,
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
atomicUnits,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
}: MultiPositionProps) => {
|
|
||||||
const positions = useMemo(
|
|
||||||
() =>
|
|
||||||
atomicUnits.map((atomicUnit) => {
|
|
||||||
const elementsInUnit = Object.keys(atomicUnit)
|
|
||||||
.map((id) => elementsMap.get(id))
|
|
||||||
.filter((el) => el !== undefined) as ExcalidrawElement[];
|
|
||||||
|
|
||||||
// we're dealing with a group
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const [x1, y1] = getCommonBounds(elementsInUnit);
|
|
||||||
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
|
|
||||||
}
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = rotate(el.x, el.y, cx, cy, el.angle);
|
|
||||||
|
|
||||||
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
|
||||||
}),
|
|
||||||
[atomicUnits, elementsMap, property],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = new Set(positions).size === 1 ? positions[0] : "Mixed";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatsDragInput
|
|
||||||
label={property === "x" ? "X" : "Y"}
|
|
||||||
elements={elements}
|
|
||||||
dragInputCallback={handlePositionChange}
|
|
||||||
value={value}
|
|
||||||
property={property}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiPosition;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
|
||||||
import { rotate } from "../../math";
|
|
||||||
import StatsDragInput from "./DragInput";
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface PositionProps {
|
|
||||||
property: "x" | "y";
|
|
||||||
element: ExcalidrawElement;
|
|
||||||
elementsMap: ElementsMap;
|
|
||||||
scene: Scene;
|
|
||||||
appState: AppState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|
||||||
accumulatedChange,
|
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
property,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const origElement = originalElements[0];
|
|
||||||
const [cx, cy] = [
|
|
||||||
origElement.x + origElement.width / 2,
|
|
||||||
origElement.y + origElement.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
origElement.x,
|
|
||||||
origElement.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
origElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
|
||||||
const newTopLeftX = property === "x" ? nextValue : topLeftX;
|
|
||||||
const newTopLeftY = property === "y" ? nextValue : topLeftY;
|
|
||||||
moveElement(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeInTopX = property === "x" ? accumulatedChange : 0;
|
|
||||||
const changeInTopY = property === "y" ? accumulatedChange : 0;
|
|
||||||
|
|
||||||
const newTopLeftX =
|
|
||||||
property === "x"
|
|
||||||
? Math.round(
|
|
||||||
shouldChangeByStepSize
|
|
||||||
? getStepSizedValue(origElement.x + changeInTopX, STEP_SIZE)
|
|
||||||
: topLeftX + changeInTopX,
|
|
||||||
)
|
|
||||||
: topLeftX;
|
|
||||||
|
|
||||||
const newTopLeftY =
|
|
||||||
property === "y"
|
|
||||||
? Math.round(
|
|
||||||
shouldChangeByStepSize
|
|
||||||
? getStepSizedValue(origElement.y + changeInTopY, STEP_SIZE)
|
|
||||||
: topLeftY + changeInTopY,
|
|
||||||
)
|
|
||||||
: topLeftY;
|
|
||||||
|
|
||||||
moveElement(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Position = ({
|
|
||||||
property,
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
scene,
|
|
||||||
appState,
|
|
||||||
}: PositionProps) => {
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
element.x,
|
|
||||||
element.y,
|
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const value =
|
|
||||||
Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatsDragInput
|
|
||||||
label={property === "x" ? "X" : "Y"}
|
|
||||||
elements={[element]}
|
|
||||||
dragInputCallback={handlePositionChange}
|
|
||||||
value={value}
|
|
||||||
property={property}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Position;
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState, memo } from "react";
|
|
||||||
import { getCommonBounds } from "../../element/bounds";
|
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
|
||||||
import { t } from "../../i18n";
|
|
||||||
import type { AppState, ExcalidrawProps } from "../../types";
|
|
||||||
import { CloseIcon } from "../icons";
|
|
||||||
import { Island } from "../Island";
|
|
||||||
import { throttle } from "lodash";
|
|
||||||
import Dimension from "./Dimension";
|
|
||||||
import Angle from "./Angle";
|
|
||||||
|
|
||||||
import FontSize from "./FontSize";
|
|
||||||
import MultiDimension from "./MultiDimension";
|
|
||||||
import { elementsAreInSameGroup } from "../../groups";
|
|
||||||
import MultiAngle from "./MultiAngle";
|
|
||||||
import MultiFontSize from "./MultiFontSize";
|
|
||||||
import Position from "./Position";
|
|
||||||
import MultiPosition from "./MultiPosition";
|
|
||||||
import Collapsible from "./Collapsible";
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
|
||||||
import { getAtomicUnits } from "./utils";
|
|
||||||
import { STATS_PANELS } from "../../constants";
|
|
||||||
|
|
||||||
interface StatsProps {
|
|
||||||
scene: Scene;
|
|
||||||
onClose: () => void;
|
|
||||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATS_TIMEOUT = 50;
|
|
||||||
|
|
||||||
export const Stats = (props: StatsProps) => {
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const sceneNonce = props.scene.getSceneNonce() || 1;
|
|
||||||
const selectedElements = props.scene.getSelectedElements({
|
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
includeBoundTextElement: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatsInner
|
|
||||||
{...props}
|
|
||||||
appState={appState}
|
|
||||||
sceneNonce={sceneNonce}
|
|
||||||
selectedElements={selectedElements}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StatsInner = memo(
|
|
||||||
({
|
|
||||||
scene,
|
|
||||||
onClose,
|
|
||||||
renderCustomStats,
|
|
||||||
selectedElements,
|
|
||||||
appState,
|
|
||||||
sceneNonce,
|
|
||||||
}: StatsProps & {
|
|
||||||
sceneNonce: number;
|
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
appState: AppState;
|
|
||||||
}) => {
|
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
|
||||||
|
|
||||||
const singleElement =
|
|
||||||
selectedElements.length === 1 ? selectedElements[0] : null;
|
|
||||||
|
|
||||||
const multipleElements =
|
|
||||||
selectedElements.length > 1 ? selectedElements : null;
|
|
||||||
|
|
||||||
const [sceneDimension, setSceneDimension] = useState<{
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}>({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const throttledSetSceneDimension = useMemo(
|
|
||||||
() =>
|
|
||||||
throttle((elements: readonly NonDeletedExcalidrawElement[]) => {
|
|
||||||
const boundingBox = getCommonBounds(elements);
|
|
||||||
setSceneDimension({
|
|
||||||
width: Math.round(boundingBox[2]) - Math.round(boundingBox[0]),
|
|
||||||
height: Math.round(boundingBox[3]) - Math.round(boundingBox[1]),
|
|
||||||
});
|
|
||||||
}, STATS_TIMEOUT),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
throttledSetSceneDimension(elements);
|
|
||||||
}, [sceneNonce, elements, throttledSetSceneDimension]);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => throttledSetSceneDimension.cancel(),
|
|
||||||
[throttledSetSceneDimension],
|
|
||||||
);
|
|
||||||
|
|
||||||
const atomicUnits = useMemo(() => {
|
|
||||||
return getAtomicUnits(selectedElements, appState);
|
|
||||||
}, [selectedElements, appState]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="Stats">
|
|
||||||
<Island padding={3}>
|
|
||||||
<div className="title">
|
|
||||||
<h2>{t("stats.title")}</h2>
|
|
||||||
<div className="close" onClick={onClose}>
|
|
||||||
{CloseIcon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Collapsible
|
|
||||||
label={<h3>{t("stats.generalStats")}</h3>}
|
|
||||||
open={!!(appState.stats.panels & STATS_PANELS.generalStats)}
|
|
||||||
openTrigger={() =>
|
|
||||||
setAppState((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
stats: {
|
|
||||||
open: true,
|
|
||||||
panels: state.stats.panels ^ STATS_PANELS.generalStats,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th colSpan={2}>{t("stats.scene")}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("stats.elements")}</td>
|
|
||||||
<td>{elements.length}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("stats.width")}</td>
|
|
||||||
<td>{sceneDimension.width}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("stats.height")}</td>
|
|
||||||
<td>{sceneDimension.height}</td>
|
|
||||||
</tr>
|
|
||||||
{renderCustomStats?.(elements, appState)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
{selectedElements.length > 0 && (
|
|
||||||
<div
|
|
||||||
id="elementStats"
|
|
||||||
style={{
|
|
||||||
marginTop: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Collapsible
|
|
||||||
label={<h3>{t("stats.elementProperties")}</h3>}
|
|
||||||
open={
|
|
||||||
!!(appState.stats.panels & STATS_PANELS.elementProperties)
|
|
||||||
}
|
|
||||||
openTrigger={() =>
|
|
||||||
setAppState((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
stats: {
|
|
||||||
open: true,
|
|
||||||
panels:
|
|
||||||
state.stats.panels ^ STATS_PANELS.elementProperties,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{singleElement && (
|
|
||||||
<div className="sectionContent">
|
|
||||||
<div className="elementType">
|
|
||||||
{t(`element.${singleElement.type}`)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="statsItem">
|
|
||||||
<Position
|
|
||||||
element={singleElement}
|
|
||||||
property="x"
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<Position
|
|
||||||
element={singleElement}
|
|
||||||
property="y"
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<Dimension
|
|
||||||
property="width"
|
|
||||||
element={singleElement}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<Dimension
|
|
||||||
property="height"
|
|
||||||
element={singleElement}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<Angle
|
|
||||||
property="angle"
|
|
||||||
element={singleElement}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<FontSize
|
|
||||||
property="fontSize"
|
|
||||||
element={singleElement}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{multipleElements && (
|
|
||||||
<div className="sectionContent">
|
|
||||||
{elementsAreInSameGroup(multipleElements) && (
|
|
||||||
<div className="elementType">{t("element.group")}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="elementsCount">
|
|
||||||
<div>{t("stats.elements")}</div>
|
|
||||||
<div>{selectedElements.length}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="statsItem">
|
|
||||||
<MultiPosition
|
|
||||||
property="x"
|
|
||||||
elements={multipleElements}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
atomicUnits={atomicUnits}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<MultiPosition
|
|
||||||
property="y"
|
|
||||||
elements={multipleElements}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
atomicUnits={atomicUnits}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<MultiDimension
|
|
||||||
property="width"
|
|
||||||
elements={multipleElements}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
atomicUnits={atomicUnits}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<MultiDimension
|
|
||||||
property="height"
|
|
||||||
elements={multipleElements}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
atomicUnits={atomicUnits}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<MultiAngle
|
|
||||||
property="angle"
|
|
||||||
elements={multipleElements}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
/>
|
|
||||||
<MultiFontSize
|
|
||||||
property="fontSize"
|
|
||||||
elements={multipleElements}
|
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Island>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(prev, next) => {
|
|
||||||
return (
|
|
||||||
prev.sceneNonce === next.sceneNonce &&
|
|
||||||
prev.selectedElements === next.selectedElements &&
|
|
||||||
prev.appState.stats.panels === next.appState.stats.panels
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,756 +0,0 @@
|
|||||||
import { fireEvent, queryByTestId } from "@testing-library/react";
|
|
||||||
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
|
|
||||||
import { getStepSizedValue } from "./utils";
|
|
||||||
import {
|
|
||||||
GlobalTestState,
|
|
||||||
mockBoundingClientRect,
|
|
||||||
render,
|
|
||||||
restoreOriginalGetBoundingClientRect,
|
|
||||||
} from "../../tests/test-utils";
|
|
||||||
import * as StaticScene from "../../renderer/staticScene";
|
|
||||||
import { vi } from "vitest";
|
|
||||||
import { reseed } from "../../random";
|
|
||||||
import { setDateTimeForTests } from "../../utils";
|
|
||||||
import { Excalidraw, mutateElement } from "../..";
|
|
||||||
import { t } from "../../i18n";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
} from "../../element/types";
|
|
||||||
import { degreeToRadian, rotate } from "../../math";
|
|
||||||
import { getTextEditor, updateTextEditor } from "../../tests/queries/dom";
|
|
||||||
import { getCommonBounds, isTextElement } from "../../element";
|
|
||||||
import { API } from "../../tests/helpers/api";
|
|
||||||
import { actionGroup } from "../../actions";
|
|
||||||
import { isInGroup } from "../../groups";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const { h } = window;
|
|
||||||
const mouse = new Pointer("mouse");
|
|
||||||
const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
|
|
||||||
let stats: HTMLElement | null = null;
|
|
||||||
let elementStats: HTMLElement | null | undefined = null;
|
|
||||||
|
|
||||||
const editInput = (input: HTMLInputElement, value: string) => {
|
|
||||||
input.focus();
|
|
||||||
fireEvent.change(input, { target: { value } });
|
|
||||||
input.blur();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatsProperty = (label: string) => {
|
|
||||||
const elementStats = UI.queryStats()?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
if (elementStats) {
|
|
||||||
const properties = elementStats?.querySelector(".statsItem");
|
|
||||||
return (
|
|
||||||
properties?.querySelector?.(
|
|
||||||
`.drag-input-container[data-testid="${label}"]`,
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const testInputProperty = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
property: "x" | "y" | "width" | "height" | "angle" | "fontSize",
|
|
||||||
label: string,
|
|
||||||
initialValue: number,
|
|
||||||
nextValue: number,
|
|
||||||
) => {
|
|
||||||
const input = getStatsProperty(label)?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(input).toBeDefined();
|
|
||||||
expect(input.value).toBe(initialValue.toString());
|
|
||||||
editInput(input, String(nextValue));
|
|
||||||
if (property === "angle") {
|
|
||||||
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
|
||||||
} else if (property === "fontSize" && isTextElement(element)) {
|
|
||||||
expect(element[property]).toBe(Number(nextValue));
|
|
||||||
} else if (property !== "fontSize") {
|
|
||||||
expect(element[property]).toBe(Number(nextValue));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("step sized value", () => {
|
|
||||||
it("should return edge values correctly", () => {
|
|
||||||
const steps = [10, 15, 20, 25, 30];
|
|
||||||
const values = [10, 15, 20, 25, 30];
|
|
||||||
|
|
||||||
steps.forEach((step, idx) => {
|
|
||||||
expect(getStepSizedValue(values[idx], step)).toEqual(values[idx]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("step sized value lies in the middle", () => {
|
|
||||||
let stepSize = 15;
|
|
||||||
let values = [7.5, 9, 12, 14.99, 15, 22.49];
|
|
||||||
|
|
||||||
values.forEach((value) => {
|
|
||||||
expect(getStepSizedValue(value, stepSize)).toEqual(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
stepSize = 10;
|
|
||||||
values = [-5, 4.99, 0, 1.23];
|
|
||||||
values.forEach((value) => {
|
|
||||||
expect(getStepSizedValue(value, stepSize)).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("binding with linear elements", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
localStorage.clear();
|
|
||||||
renderStaticScene.mockClear();
|
|
||||||
reseed(19);
|
|
||||||
setDateTimeForTests("201933152653");
|
|
||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
|
|
||||||
h.elements = [];
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
stats = UI.queryStats();
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(200, 100);
|
|
||||||
|
|
||||||
UI.clickTool("arrow");
|
|
||||||
mouse.down(5, 0);
|
|
||||||
mouse.up(300, 50);
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreOriginalGetBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remain bound to linear element on small position change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputX = getStatsProperty("X")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
expect(inputX).not.toBeNull();
|
|
||||||
editInput(inputX, String("204"));
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remain bound to linear element on small angle change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputAngle = getStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
editInput(inputAngle, String("1"));
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should unbind linear element on large position change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputX = getStatsProperty("X")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
expect(inputX).not.toBeNull();
|
|
||||||
editInput(inputX, String("254"));
|
|
||||||
expect(linear.startBinding).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remain bound to linear element on small angle change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputAngle = getStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
editInput(inputAngle, String("45"));
|
|
||||||
expect(linear.startBinding).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// single element
|
|
||||||
describe("stats for a generic element", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
localStorage.clear();
|
|
||||||
renderStaticScene.mockClear();
|
|
||||||
reseed(7);
|
|
||||||
setDateTimeForTests("201933152653");
|
|
||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
|
|
||||||
h.elements = [];
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
stats = UI.queryStats();
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(200, 100);
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreOriginalGetBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should open stats", () => {
|
|
||||||
expect(stats).toBeDefined();
|
|
||||||
expect(elementStats).toBeDefined();
|
|
||||||
|
|
||||||
// title
|
|
||||||
const title = elementStats?.querySelector("h3");
|
|
||||||
expect(title?.lastChild?.nodeValue)?.toBe(t("stats.elementProperties"));
|
|
||||||
|
|
||||||
// element type
|
|
||||||
const elementType = elementStats?.querySelector(".elementType");
|
|
||||||
expect(elementType).toBeDefined();
|
|
||||||
expect(elementType?.lastChild?.nodeValue).toBe(t("element.rectangle"));
|
|
||||||
|
|
||||||
// properties
|
|
||||||
const properties = elementStats?.querySelector(".statsItem");
|
|
||||||
expect(properties?.childNodes).toBeDefined();
|
|
||||||
["X", "Y", "W", "H", "A"].forEach((label) => () => {
|
|
||||||
expect(
|
|
||||||
properties?.querySelector?.(
|
|
||||||
`.drag-input-container[data-testid="${label}"]`,
|
|
||||||
),
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be able to edit all properties for a general element", () => {
|
|
||||||
const rectangle = h.elements[0];
|
|
||||||
const initialX = rectangle.x;
|
|
||||||
const initialY = rectangle.y;
|
|
||||||
|
|
||||||
testInputProperty(rectangle, "width", "W", 200, 100);
|
|
||||||
testInputProperty(rectangle, "height", "H", 100, 200);
|
|
||||||
testInputProperty(rectangle, "x", "X", initialX, 230);
|
|
||||||
testInputProperty(rectangle, "y", "Y", initialY, 220);
|
|
||||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should keep only two decimal places", () => {
|
|
||||||
const rectangle = h.elements[0];
|
|
||||||
const rectangleId = rectangle.id;
|
|
||||||
|
|
||||||
const input = getStatsProperty("W")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(input).toBeDefined();
|
|
||||||
expect(input.value).toBe(rectangle.width.toString());
|
|
||||||
editInput(input, "123.123");
|
|
||||||
expect(h.elements.length).toBe(1);
|
|
||||||
expect(rectangle.id).toBe(rectangleId);
|
|
||||||
expect(input.value).toBe("123.12");
|
|
||||||
expect(rectangle.width).toBe(123.12);
|
|
||||||
|
|
||||||
editInput(input, "88.98766");
|
|
||||||
expect(input.value).toBe("88.99");
|
|
||||||
expect(rectangle.width).toBe(88.99);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update input x and y when angle is changed", () => {
|
|
||||||
const rectangle = h.elements[0];
|
|
||||||
const [cx, cy] = [
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const xInput = getStatsProperty("X")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
const yInput = getStatsProperty("Y")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(xInput.value).toBe(topLeftX.toString());
|
|
||||||
expect(yInput.value).toBe(topLeftY.toString());
|
|
||||||
|
|
||||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
|
||||||
|
|
||||||
let [newTopLeftX, newTopLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
|
||||||
expect(newTopLeftY.toString()).not.toEqual(yInput.value);
|
|
||||||
|
|
||||||
testInputProperty(rectangle, "angle", "A", 45, 66);
|
|
||||||
|
|
||||||
[newTopLeftX, newTopLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
expect(newTopLeftX.toString()).not.toEqual(xInput.value);
|
|
||||||
expect(newTopLeftY.toString()).not.toEqual(yInput.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fix top left corner when width or height is changed", () => {
|
|
||||||
const rectangle = h.elements[0];
|
|
||||||
|
|
||||||
testInputProperty(rectangle, "angle", "A", 0, 45);
|
|
||||||
let [cx, cy] = [
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
testInputProperty(rectangle, "width", "W", rectangle.width, 400);
|
|
||||||
[cx, cy] = [
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
];
|
|
||||||
let [currentTopLeftX, currentTopLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
|
||||||
expect(currentTopLeftY).toBeCloseTo(topLeftY, 4);
|
|
||||||
|
|
||||||
testInputProperty(rectangle, "height", "H", rectangle.height, 400);
|
|
||||||
[cx, cy] = [
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
];
|
|
||||||
[currentTopLeftX, currentTopLeftY] = rotate(
|
|
||||||
rectangle.x,
|
|
||||||
rectangle.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
rectangle.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
|
|
||||||
expect(currentTopLeftY).toBeCloseTo(topLeftY, 4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stats for a non-generic element", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
localStorage.clear();
|
|
||||||
renderStaticScene.mockClear();
|
|
||||||
reseed(7);
|
|
||||||
setDateTimeForTests("201933152653");
|
|
||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
|
|
||||||
h.elements = [];
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
stats = UI.queryStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreOriginalGetBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("text element", async () => {
|
|
||||||
UI.clickTool("text");
|
|
||||||
mouse.clickAt(20, 30);
|
|
||||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello!");
|
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
const text = h.elements[0] as ExcalidrawTextElement;
|
|
||||||
mouse.clickOn(text);
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
// can change font size
|
|
||||||
const input = getStatsProperty("F")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(input).toBeDefined();
|
|
||||||
expect(input.value).toBe(text.fontSize.toString());
|
|
||||||
editInput(input, "36");
|
|
||||||
expect(text.fontSize).toBe(36);
|
|
||||||
|
|
||||||
// cannot change width or height
|
|
||||||
const width = getStatsProperty("W")?.querySelector(".drag-input");
|
|
||||||
expect(width).toBeUndefined();
|
|
||||||
const height = getStatsProperty("H")?.querySelector(".drag-input");
|
|
||||||
expect(height).toBeUndefined();
|
|
||||||
|
|
||||||
// min font size is 4
|
|
||||||
editInput(input, "0");
|
|
||||||
expect(text.fontSize).not.toBe(0);
|
|
||||||
expect(text.fontSize).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("frame element", () => {
|
|
||||||
const frame = API.createElement({
|
|
||||||
id: "id0",
|
|
||||||
type: "frame",
|
|
||||||
x: 150,
|
|
||||||
width: 150,
|
|
||||||
});
|
|
||||||
h.elements = [frame];
|
|
||||||
h.setState({
|
|
||||||
selectedElementIds: {
|
|
||||||
[frame.id]: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
expect(elementStats).toBeDefined();
|
|
||||||
|
|
||||||
// cannot change angle
|
|
||||||
const angle = getStatsProperty("A")?.querySelector(".drag-input");
|
|
||||||
expect(angle).toBeUndefined();
|
|
||||||
|
|
||||||
// can change width or height
|
|
||||||
testInputProperty(frame, "width", "W", frame.width, 250);
|
|
||||||
testInputProperty(frame, "height", "H", frame.height, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("image element", () => {
|
|
||||||
const image = API.createElement({ type: "image", width: 200, height: 100 });
|
|
||||||
h.elements = [image];
|
|
||||||
mouse.clickOn(image);
|
|
||||||
h.setState({
|
|
||||||
selectedElementIds: {
|
|
||||||
[image.id]: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
expect(elementStats).toBeDefined();
|
|
||||||
const widthToHeight = image.width / image.height;
|
|
||||||
|
|
||||||
// when width or height is changed, the aspect ratio is preserved
|
|
||||||
testInputProperty(image, "width", "W", image.width, 400);
|
|
||||||
expect(image.width).toBe(400);
|
|
||||||
expect(image.width / image.height).toBe(widthToHeight);
|
|
||||||
|
|
||||||
testInputProperty(image, "height", "H", image.height, 80);
|
|
||||||
expect(image.height).toBe(80);
|
|
||||||
expect(image.width / image.height).toBe(widthToHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display fontSize for bound text", () => {
|
|
||||||
const container = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
const text = API.createElement({
|
|
||||||
type: "text",
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
containerId: container.id,
|
|
||||||
fontSize: 20,
|
|
||||||
});
|
|
||||||
mutateElement(container, {
|
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
|
||||||
});
|
|
||||||
h.elements = [container, text];
|
|
||||||
|
|
||||||
API.setSelectedElements([container]);
|
|
||||||
const fontSize = getStatsProperty("F")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(fontSize).toBeDefined();
|
|
||||||
|
|
||||||
editInput(fontSize, "40");
|
|
||||||
|
|
||||||
expect(text.fontSize).toBe(40);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// multiple elements
|
|
||||||
describe("stats for multiple elements", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
mouse.reset();
|
|
||||||
localStorage.clear();
|
|
||||||
renderStaticScene.mockClear();
|
|
||||||
reseed(7);
|
|
||||||
setDateTimeForTests("201933152653");
|
|
||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
|
|
||||||
h.elements = [];
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
stats = UI.queryStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreOriginalGetBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display MIXED for elements with different values", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(200, 100);
|
|
||||||
|
|
||||||
UI.clickTool("ellipse");
|
|
||||||
mouse.down(50, 50);
|
|
||||||
mouse.up(100, 100);
|
|
||||||
|
|
||||||
UI.clickTool("diamond");
|
|
||||||
mouse.down(-100, -100);
|
|
||||||
mouse.up(125, 145);
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
|
||||||
acc[el.id] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, true>),
|
|
||||||
});
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
const width = getStatsProperty("W")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(width?.value).toBe("Mixed");
|
|
||||||
const height = getStatsProperty("H")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(height?.value).toBe("Mixed");
|
|
||||||
const angle = getStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(angle.value).toBe("0");
|
|
||||||
|
|
||||||
editInput(width, "250");
|
|
||||||
h.elements.forEach((el) => {
|
|
||||||
expect(el.width).toBe(250);
|
|
||||||
});
|
|
||||||
|
|
||||||
editInput(height, "450");
|
|
||||||
h.elements.forEach((el) => {
|
|
||||||
expect(el.height).toBe(450);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display a property when one of the elements is editable for that property", async () => {
|
|
||||||
// text, rectangle, frame
|
|
||||||
UI.clickTool("text");
|
|
||||||
mouse.clickAt(20, 30);
|
|
||||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello!");
|
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(200, 100);
|
|
||||||
|
|
||||||
const frame = API.createElement({
|
|
||||||
type: "frame",
|
|
||||||
x: 150,
|
|
||||||
width: 150,
|
|
||||||
});
|
|
||||||
|
|
||||||
h.elements = [...h.elements, frame];
|
|
||||||
|
|
||||||
const text = h.elements.find((el) => el.type === "text");
|
|
||||||
const rectangle = h.elements.find((el) => el.type === "rectangle");
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
|
||||||
acc[el.id] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, true>),
|
|
||||||
});
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
const width = getStatsProperty("W")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(width).toBeDefined();
|
|
||||||
expect(width.value).toBe("Mixed");
|
|
||||||
|
|
||||||
const height = getStatsProperty("H")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(height).toBeDefined();
|
|
||||||
expect(height.value).toBe("Mixed");
|
|
||||||
|
|
||||||
const angle = getStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(angle).toBeDefined();
|
|
||||||
expect(angle.value).toBe("0");
|
|
||||||
|
|
||||||
const fontSize = getStatsProperty("F")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(fontSize).toBeDefined();
|
|
||||||
|
|
||||||
// changing width does not affect text
|
|
||||||
editInput(width, "200");
|
|
||||||
|
|
||||||
expect(rectangle?.width).toBe(200);
|
|
||||||
expect(frame.width).toBe(200);
|
|
||||||
expect(text?.width).not.toBe(200);
|
|
||||||
|
|
||||||
editInput(angle, "40");
|
|
||||||
|
|
||||||
const angleInRadian = degreeToRadian(40);
|
|
||||||
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
|
||||||
expect(text?.angle).toBeCloseTo(angleInRadian, 4);
|
|
||||||
expect(frame.angle).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should treat groups as single units", () => {
|
|
||||||
const createAndSelectGroup = () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(100, 100);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(0, 0);
|
|
||||||
mouse.up(100, 100);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
|
||||||
mouse.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
h.app.actionManager.executeAction(actionGroup);
|
|
||||||
};
|
|
||||||
|
|
||||||
createAndSelectGroup();
|
|
||||||
|
|
||||||
const elementsInGroup = h.elements.filter((el) => isInGroup(el));
|
|
||||||
let [x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
|
||||||
|
|
||||||
const x = getStatsProperty("X")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(x).toBeDefined();
|
|
||||||
expect(Number(x.value)).toBe(x1);
|
|
||||||
|
|
||||||
editInput(x, "300");
|
|
||||||
|
|
||||||
expect(h.elements[0].x).toBe(300);
|
|
||||||
expect(h.elements[1].x).toBe(400);
|
|
||||||
expect(x.value).toBe("300");
|
|
||||||
|
|
||||||
const y = getStatsProperty("Y")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(y).toBeDefined();
|
|
||||||
expect(Number(y.value)).toBe(y1);
|
|
||||||
|
|
||||||
editInput(y, "200");
|
|
||||||
|
|
||||||
expect(h.elements[0].y).toBe(200);
|
|
||||||
expect(h.elements[1].y).toBe(300);
|
|
||||||
expect(y.value).toBe("200");
|
|
||||||
|
|
||||||
const width = getStatsProperty("W")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(width).toBeDefined();
|
|
||||||
expect(Number(width.value)).toBe(200);
|
|
||||||
|
|
||||||
const height = getStatsProperty("H")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(height).toBeDefined();
|
|
||||||
expect(Number(height.value)).toBe(200);
|
|
||||||
|
|
||||||
editInput(width, "400");
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
|
||||||
let newGroupWidth = x2 - x1;
|
|
||||||
|
|
||||||
expect(newGroupWidth).toBeCloseTo(400, 4);
|
|
||||||
|
|
||||||
editInput(width, "300");
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
|
||||||
newGroupWidth = x2 - x1;
|
|
||||||
expect(newGroupWidth).toBeCloseTo(300, 4);
|
|
||||||
|
|
||||||
editInput(height, "500");
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
|
||||||
const newGroupHeight = y2 - y1;
|
|
||||||
expect(newGroupHeight).toBeCloseTo(500, 4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import {
|
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
updateBoundElements,
|
|
||||||
} from "../../element/binding";
|
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
|
||||||
import {
|
|
||||||
measureFontSizeFromWidth,
|
|
||||||
rescalePointsInElement,
|
|
||||||
} from "../../element/resizeElements";
|
|
||||||
import {
|
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
|
||||||
getBoundTextMaxWidth,
|
|
||||||
handleBindTextResize,
|
|
||||||
} from "../../element/textElement";
|
|
||||||
import {
|
|
||||||
isFrameLikeElement,
|
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
|
||||||
} from "../../element/typeChecks";
|
|
||||||
import type {
|
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../../element/types";
|
|
||||||
import {
|
|
||||||
getSelectedGroupIds,
|
|
||||||
getElementsInGroup,
|
|
||||||
isInGroup,
|
|
||||||
} from "../../groups";
|
|
||||||
import { rotate } from "../../math";
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
import { getFontString } from "../../utils";
|
|
||||||
|
|
||||||
export type StatsInputProperty =
|
|
||||||
| "x"
|
|
||||||
| "y"
|
|
||||||
| "width"
|
|
||||||
| "height"
|
|
||||||
| "angle"
|
|
||||||
| "fontSize";
|
|
||||||
|
|
||||||
export const SMALLEST_DELTA = 0.01;
|
|
||||||
|
|
||||||
export const isPropertyEditable = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
property: keyof ExcalidrawElement,
|
|
||||||
) => {
|
|
||||||
if (property === "height" && isTextElement(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (property === "width" && isTextElement(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (property === "angle" && isFrameLikeElement(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStepSizedValue = (value: number, stepSize: number) => {
|
|
||||||
const v = value + stepSize / 2;
|
|
||||||
return v - (v % stepSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AtomicUnit = Record<string, true>;
|
|
||||||
export const getElementsInAtomicUnit = (
|
|
||||||
atomicUnit: AtomicUnit,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap?: ElementsMap,
|
|
||||||
) => {
|
|
||||||
return Object.keys(atomicUnit)
|
|
||||||
.map((id) => ({
|
|
||||||
original: (originalElementsMap ?? elementsMap).get(id),
|
|
||||||
latest: elementsMap.get(id),
|
|
||||||
}))
|
|
||||||
.filter((el) => el.original !== undefined && el.latest !== undefined) as {
|
|
||||||
original: NonDeletedExcalidrawElement;
|
|
||||||
latest: NonDeletedExcalidrawElement;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newOrigin = (
|
|
||||||
x1: number,
|
|
||||||
y1: number,
|
|
||||||
w1: number,
|
|
||||||
h1: number,
|
|
||||||
w2: number,
|
|
||||||
h2: number,
|
|
||||||
angle: number,
|
|
||||||
) => {
|
|
||||||
/**
|
|
||||||
* The formula below is the result of solving
|
|
||||||
* rotate(x1, y1, cx1, cy1, angle) = rotate(x2, y2, cx2, cy2, angle)
|
|
||||||
* where rotate is the function defined in math.ts
|
|
||||||
*
|
|
||||||
* This is so that the new origin (x2, y2),
|
|
||||||
* when rotated against the new center (cx2, cy2),
|
|
||||||
* coincides with (x1, y1) rotated against (cx1, cy1)
|
|
||||||
*
|
|
||||||
* The reason for doing this computation is so the element's top left corner
|
|
||||||
* on the canvas remains fixed after any changes in its dimension.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return {
|
|
||||||
x:
|
|
||||||
x1 +
|
|
||||||
(w1 - w2) / 2 +
|
|
||||||
((w2 - w1) / 2) * Math.cos(angle) +
|
|
||||||
((h1 - h2) / 2) * Math.sin(angle),
|
|
||||||
y:
|
|
||||||
y1 +
|
|
||||||
(h1 - h2) / 2 +
|
|
||||||
((w2 - w1) / 2) * Math.sin(angle) +
|
|
||||||
((h2 - h1) / 2) * Math.cos(angle),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resizeElement = (
|
|
||||||
nextWidth: number,
|
|
||||||
nextHeight: number,
|
|
||||||
keepAspectRatio: boolean,
|
|
||||||
origElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
shouldInformMutation = true,
|
|
||||||
) => {
|
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
|
||||||
if (!latestElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
|
||||||
|
|
||||||
if (boundTextElement) {
|
|
||||||
const minWidth = getApproxMinLineWidth(
|
|
||||||
getFontString(boundTextElement),
|
|
||||||
boundTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
const minHeight = getApproxMinLineHeight(
|
|
||||||
boundTextElement.fontSize,
|
|
||||||
boundTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
nextWidth = Math.max(nextWidth, minWidth);
|
|
||||||
nextHeight = Math.max(nextHeight, minHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
...newOrigin(
|
|
||||||
latestElement.x,
|
|
||||||
latestElement.y,
|
|
||||||
latestElement.width,
|
|
||||||
latestElement.height,
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
latestElement.angle,
|
|
||||||
),
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
...rescalePointsInElement(origElement, nextWidth, nextHeight, true),
|
|
||||||
},
|
|
||||||
shouldInformMutation,
|
|
||||||
);
|
|
||||||
updateBindings(latestElement, elementsMap, {
|
|
||||||
newSize: {
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (boundTextElement) {
|
|
||||||
boundTextFont = {
|
|
||||||
fontSize: boundTextElement.fontSize,
|
|
||||||
};
|
|
||||||
if (keepAspectRatio) {
|
|
||||||
const updatedElement = {
|
|
||||||
...latestElement,
|
|
||||||
width: nextWidth,
|
|
||||||
height: nextHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextFont = measureFontSizeFromWidth(
|
|
||||||
boundTextElement,
|
|
||||||
elementsMap,
|
|
||||||
getBoundTextMaxWidth(updatedElement, boundTextElement),
|
|
||||||
);
|
|
||||||
boundTextFont = {
|
|
||||||
fontSize: nextFont?.size ?? boundTextElement.fontSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont) {
|
|
||||||
mutateElement(boundTextElement, {
|
|
||||||
fontSize: boundTextFont.fontSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleBindTextResize(latestElement, elementsMap, "e", keepAspectRatio);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const moveElement = (
|
|
||||||
newTopLeftX: number,
|
|
||||||
newTopLeftY: number,
|
|
||||||
originalElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
shouldInformMutation = true,
|
|
||||||
) => {
|
|
||||||
const latestElement = elementsMap.get(originalElement.id);
|
|
||||||
if (!latestElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [cx, cy] = [
|
|
||||||
originalElement.x + originalElement.width / 2,
|
|
||||||
originalElement.y + originalElement.height / 2,
|
|
||||||
];
|
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
originalElement.x,
|
|
||||||
originalElement.y,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
originalElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeInX = newTopLeftX - topLeftX;
|
|
||||||
const changeInY = newTopLeftY - topLeftY;
|
|
||||||
|
|
||||||
const [x, y] = rotate(
|
|
||||||
newTopLeftX,
|
|
||||||
newTopLeftY,
|
|
||||||
cx + changeInX,
|
|
||||||
cy + changeInY,
|
|
||||||
-originalElement.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
},
|
|
||||||
shouldInformMutation,
|
|
||||||
);
|
|
||||||
updateBindings(latestElement, elementsMap);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
|
||||||
originalElement,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
if (boundTextElement) {
|
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
|
||||||
latestBoundTextElement &&
|
|
||||||
mutateElement(
|
|
||||||
latestBoundTextElement,
|
|
||||||
{
|
|
||||||
x: boundTextElement.x + changeInX,
|
|
||||||
y: boundTextElement.y + changeInY,
|
|
||||||
},
|
|
||||||
shouldInformMutation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAtomicUnits = (
|
|
||||||
targetElements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const selectedGroupIds = getSelectedGroupIds(appState);
|
|
||||||
const _atomicUnits = selectedGroupIds.map((gid) => {
|
|
||||||
return getElementsInGroup(targetElements, gid).reduce((acc, el) => {
|
|
||||||
acc[el.id] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as AtomicUnit);
|
|
||||||
});
|
|
||||||
targetElements
|
|
||||||
.filter((el) => !isInGroup(el))
|
|
||||||
.forEach((el) => {
|
|
||||||
_atomicUnits.push({
|
|
||||||
[el.id]: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return _atomicUnits;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateBindings = (
|
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
options?: {
|
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
|
||||||
newSize?: { width: number; height: number };
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if (isLinearElement(latestElement)) {
|
|
||||||
bindOrUnbindLinearElements([latestElement], elementsMap, true, []);
|
|
||||||
} else {
|
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { MermaidConfig } from "@excalidraw/mermaid-to-excalidraw";
|
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
|
||||||
import type { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
import type { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
||||||
import { DEFAULT_EXPORT_PADDING, EDITOR_LS_KEYS } from "../../constants";
|
import {
|
||||||
|
DEFAULT_EXPORT_PADDING,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
EDITOR_LS_KEYS,
|
||||||
|
} from "../../constants";
|
||||||
import { convertToExcalidrawElements, exportToCanvas } from "../../index";
|
import { convertToExcalidrawElements, exportToCanvas } from "../../index";
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
import type { AppClassProperties, BinaryFiles } from "../../types";
|
import type { AppClassProperties, BinaryFiles } from "../../types";
|
||||||
@@ -34,7 +38,7 @@ export interface MermaidToExcalidrawLibProps {
|
|||||||
api: Promise<{
|
api: Promise<{
|
||||||
parseMermaidToExcalidraw: (
|
parseMermaidToExcalidraw: (
|
||||||
definition: string,
|
definition: string,
|
||||||
config?: MermaidConfig,
|
options: MermaidOptions,
|
||||||
) => Promise<MermaidToExcalidrawResult>;
|
) => Promise<MermaidToExcalidrawResult>;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -74,10 +78,15 @@ export const convertMermaidToExcalidraw = async ({
|
|||||||
|
|
||||||
let ret;
|
let ret;
|
||||||
try {
|
try {
|
||||||
ret = await api.parseMermaidToExcalidraw(mermaidDefinition);
|
ret = await api.parseMermaidToExcalidraw(mermaidDefinition, {
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ret = await api.parseMermaidToExcalidraw(
|
ret = await api.parseMermaidToExcalidraw(
|
||||||
mermaidDefinition.replace(/"/g, "'"),
|
mermaidDefinition.replace(/"/g, "'"),
|
||||||
|
{
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { elements, files } = ret;
|
const { elements, files } = ret;
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import type {
|
|||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
} from "../../scene/types";
|
} from "../../scene/types";
|
||||||
import type {
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../../element/types";
|
|
||||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||||
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
||||||
|
|
||||||
@@ -22,7 +19,6 @@ type InteractiveCanvasProps = {
|
|||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
allElementsMap: NonDeletedSceneElementsMap;
|
|
||||||
sceneNonce: number | undefined;
|
sceneNonce: number | undefined;
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
@@ -126,7 +122,6 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||||||
elementsMap: props.elementsMap,
|
elementsMap: props.elementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
selectedElements: props.selectedElements,
|
selectedElements: props.selectedElements,
|
||||||
allElementsMap: props.allElementsMap,
|
|
||||||
scale: window.devicePixelRatio,
|
scale: window.devicePixelRatio,
|
||||||
appState: props.appState,
|
appState: props.appState,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
@@ -202,7 +197,6 @@ const getRelevantAppStateProps = (
|
|||||||
activeEmbeddable: appState.activeEmbeddable,
|
activeEmbeddable: appState.activeEmbeddable,
|
||||||
snapLines: appState.snapLines,
|
snapLines: appState.snapLines,
|
||||||
zenModeEnabled: appState.zenModeEnabled,
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
editingElement: appState.editingElement,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|||||||
@@ -1573,18 +1573,6 @@ export const TextAlignMiddleIcon = React.memo(({ theme }: { theme: Theme }) =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const angleIcon = createIcon(
|
|
||||||
<g>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M21 19h-18l9 -15" />
|
|
||||||
<path d="M20.615 15.171h.015" />
|
|
||||||
<path d="M19.515 11.771h.015" />
|
|
||||||
<path d="M17.715 8.671h.015" />
|
|
||||||
<path d="M15.415 5.971h.015" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const publishIcon = createIcon(
|
export const publishIcon = createIcon(
|
||||||
<path
|
<path
|
||||||
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"
|
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"
|
||||||
@@ -2073,19 +2061,3 @@ export const lineEditorIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const collapseDownIcon = createIcon(
|
|
||||||
<g>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M6 9l6 6l6 -6" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const collapseUpIcon = createIcon(
|
|
||||||
<g>
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M6 15l6 -6l6 6" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ export const supportsResizeObserver =
|
|||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
// distance when creating text before it's considered `autoResize: false`
|
|
||||||
// we're using higher threshold so that clicks that end up being drags
|
|
||||||
// don't unintentionally create text elements that are wrapped to a few chars
|
|
||||||
// (happens a lot with fast clicks with the text tool)
|
|
||||||
export const TEXT_AUTOWRAP_THRESHOLD = 36; // px
|
|
||||||
export const DRAGGING_THRESHOLD = 10; // px
|
export const DRAGGING_THRESHOLD = 10; // px
|
||||||
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
||||||
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
||||||
@@ -274,7 +269,7 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
|
|||||||
|
|
||||||
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
|
|
||||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
@@ -405,7 +400,3 @@ export const EDITOR_LS_KEYS = {
|
|||||||
* where filename is optional and we can't retrieve name from app state
|
* where filename is optional and we can't retrieve name from app state
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_FILENAME = "Untitled";
|
export const DEFAULT_FILENAME = "Untitled";
|
||||||
|
|
||||||
export const STATS_PANELS = { generalStats: 1, elementProperties: 2 } as const;
|
|
||||||
|
|
||||||
export const MIN_WIDTH_OR_HEIGHT = 1;
|
|
||||||
|
|||||||
@@ -22,12 +22,6 @@
|
|||||||
--sat: env(safe-area-inset-top);
|
--sat: env(safe-area-inset-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.excalidraw-cursor-resize,
|
|
||||||
body.excalidraw-cursor-resize a:hover,
|
|
||||||
body.excalidraw-cursor-resize * {
|
|
||||||
cursor: ew-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
||||||
Roboto, Helvetica, Arial, sans-serif;
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
|||||||
@@ -123,26 +123,10 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
fileHandle?: FileSystemHandle | null,
|
fileHandle?: FileSystemHandle | null,
|
||||||
) => {
|
) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
// assume Obsidian excalidraw plugin file
|
|
||||||
if (blob.name?.endsWith(".excalidraw.md")) {
|
|
||||||
if (contents.indexOf("```compressed-json") > -1) {
|
|
||||||
let str = contents.slice(
|
|
||||||
contents.indexOf("```compressed-json") + '"```compressed-json'.length,
|
|
||||||
);
|
|
||||||
str = str.slice(0, str.indexOf("```"));
|
|
||||||
str = str.replace(/\n/g, "").replace(/\r/g, "");
|
|
||||||
const LZString = await import("lz-string");
|
|
||||||
|
|
||||||
data = JSON.parse(LZString.decompressFromBase64(str));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
data = data || JSON.parse(contents);
|
data = JSON.parse(contents);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (isSupportedImageFile(blob)) {
|
if (isSupportedImageFile(blob)) {
|
||||||
throw new ImageSceneDataError(
|
throw new ImageSceneDataError(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
@@ -22,12 +21,7 @@ import {
|
|||||||
isInvisiblySmallElement,
|
isInvisiblySmallElement,
|
||||||
refreshTextDimensions,
|
refreshTextDimensions,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import { isTextElement, isUsingAdaptiveRadius } from "../element/typeChecks";
|
||||||
isArrowElement,
|
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
|
||||||
isUsingAdaptiveRadius,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
@@ -51,7 +45,6 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getSizeFromPoints } from "../points";
|
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -147,7 +140,7 @@ const restoreElementWithProperties = <
|
|||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
groupIds: element.groupIds ?? [],
|
groupIds: element.groupIds ?? [],
|
||||||
frameId: element.frameId ?? null,
|
frameId: element.frameId ?? null,
|
||||||
roundness: element.roundness
|
roundness: typeof element.roundness !== "undefined"
|
||||||
? element.roundness
|
? element.roundness
|
||||||
: element.strokeSharpness === "round"
|
: element.strokeSharpness === "round"
|
||||||
? {
|
? {
|
||||||
@@ -277,7 +270,6 @@ const restoreElement = (
|
|||||||
points,
|
points,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
...getSizeFromPoints(points),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,23 +458,6 @@ export const restoreElements = (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(element)) {
|
|
||||||
if (
|
|
||||||
element.startBinding &&
|
|
||||||
(!restoredElementsMap.has(element.startBinding.elementId) ||
|
|
||||||
!isArrowElement(element))
|
|
||||||
) {
|
|
||||||
(element as Mutable<ExcalidrawLinearElement>).startBinding = null;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
element.endBinding &&
|
|
||||||
(!restoredElementsMap.has(element.endBinding.elementId) ||
|
|
||||||
!isArrowElement(element))
|
|
||||||
) {
|
|
||||||
(element as Mutable<ExcalidrawLinearElement>).endBinding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoredElements;
|
return restoredElements;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import type { AppState, Point } from "../types";
|
import type { AppClassProperties, AppState, Point } from "../types";
|
||||||
import { isPointOnShape } from "../../utils/collision";
|
import { isPointOnShape } from "../../utils/collision";
|
||||||
import { getElementAtPosition } from "../scene";
|
import { getElementAtPosition } from "../scene";
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +43,6 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
import { arrayToMap, tupleToCoors } from "../utils";
|
import { arrayToMap, tupleToCoors } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getElementShape } from "../shapes";
|
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
@@ -180,19 +179,19 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
edge: "start" | "end",
|
edge: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): NonDeleted<ExcalidrawElement> | null => {
|
): NonDeleted<ExcalidrawElement> | null => {
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||||
const elementId =
|
const elementId =
|
||||||
edge === "start"
|
edge === "start"
|
||||||
? linearElement.startBinding?.elementId
|
? linearElement.startBinding?.elementId
|
||||||
: linearElement.endBinding?.elementId;
|
: linearElement.endBinding?.elementId;
|
||||||
if (elementId) {
|
if (elementId) {
|
||||||
const element = elementsMap.get(elementId);
|
const element = elementsMap.get(
|
||||||
if (
|
elementId,
|
||||||
isBindableElement(element) &&
|
) as NonDeleted<ExcalidrawBindableElement>;
|
||||||
bindingBorderTest(element, coors, elementsMap)
|
if (bindingBorderTest(element, coors, app)) {
|
||||||
) {
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,13 +201,13 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
|
|
||||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||||
["start", "end"].map((edge) =>
|
["start", "end"].map((edge) =>
|
||||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
||||||
linearElement,
|
linearElement,
|
||||||
edge as "start" | "end",
|
edge as "start" | "end",
|
||||||
elementsMap,
|
app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -216,7 +215,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[],
|
draggingPoints: readonly number[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
||||||
const startIdx = 0;
|
const startIdx = 0;
|
||||||
const endIdx = selectedElement.points.length - 1;
|
const endIdx = selectedElement.points.length - 1;
|
||||||
@@ -224,57 +223,37 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||||
const start = startDragged
|
const start = startDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getElligibleElementForBindingElement(selectedElement, "start", app)
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
)
|
|
||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: // We have to update the focus and gap of the binding, so let's rebind
|
: // We have to update the focus and gap of the binding, so let's rebind
|
||||||
getElligibleElementForBindingElement(
|
getElligibleElementForBindingElement(selectedElement, "start", app);
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getElligibleElementForBindingElement(selectedElement, "end", app)
|
||||||
selectedElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
)
|
|
||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: // We have to update the focus and gap of the binding, so let's rebind
|
: // We have to update the focus and gap of the binding, so let's rebind
|
||||||
getElligibleElementForBindingElement(selectedElement, "end", elementsMap);
|
getElligibleElementForBindingElement(selectedElement, "end", app);
|
||||||
|
|
||||||
return [start, end];
|
return [start, end];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBindingStrategyForDraggingArrowOrJoints = (
|
const getBindingStrategyForDraggingArrowOrJoints = (
|
||||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
||||||
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
app,
|
||||||
);
|
);
|
||||||
const start = startIsClose
|
const start = startIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getElligibleElementForBindingElement(selectedElement, "start", app)
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
: null;
|
: null;
|
||||||
const end = endIsClose
|
const end = endIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getElligibleElementForBindingElement(selectedElement, "end", app)
|
||||||
selectedElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -283,7 +262,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
|
|
||||||
export const bindOrUnbindLinearElements = (
|
export const bindOrUnbindLinearElements = (
|
||||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[] | null,
|
draggingPoints: readonly number[] | null,
|
||||||
): void => {
|
): void => {
|
||||||
@@ -294,22 +273,27 @@ export const bindOrUnbindLinearElements = (
|
|||||||
selectedElement,
|
selectedElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
draggingPoints ?? [],
|
draggingPoints ?? [],
|
||||||
elementsMap,
|
app,
|
||||||
)
|
)
|
||||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||||
getBindingStrategyForDraggingArrowOrJoints(
|
getBindingStrategyForDraggingArrowOrJoints(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
app,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap);
|
bindOrUnbindLinearElement(
|
||||||
|
selectedElement,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSuggestedBindingsForArrows = (
|
export const getSuggestedBindingsForArrows = (
|
||||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): SuggestedBinding[] => {
|
): SuggestedBinding[] => {
|
||||||
// HOT PATH: Bail out if selected elements list is too large
|
// HOT PATH: Bail out if selected elements list is too large
|
||||||
if (selectedElements.length > 50) {
|
if (selectedElements.length > 50) {
|
||||||
@@ -320,7 +304,7 @@ export const getSuggestedBindingsForArrows = (
|
|||||||
selectedElements
|
selectedElements
|
||||||
.filter(isLinearElement)
|
.filter(isLinearElement)
|
||||||
.flatMap((element) =>
|
.flatMap((element) =>
|
||||||
getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap),
|
getOriginalBindingsIfStillCloseToArrowEnds(element, app),
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
||||||
@@ -342,20 +326,17 @@ export const maybeBindLinearElement = (
|
|||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pointerCoords: { x: number; y: number },
|
pointerCoords: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): void => {
|
): void => {
|
||||||
if (appState.startBoundElement != null) {
|
if (appState.startBoundElement != null) {
|
||||||
bindLinearElement(
|
bindLinearElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
appState.startBoundElement,
|
appState.startBoundElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
const hoveredElement = getHoveredElementForBinding(pointerCoords, app);
|
||||||
pointerCoords,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
hoveredElement != null &&
|
hoveredElement != null &&
|
||||||
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
||||||
@@ -364,7 +345,12 @@ export const maybeBindLinearElement = (
|
|||||||
"end",
|
"end",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
bindLinearElement(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
"end",
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -374,9 +360,6 @@ export const bindLinearElement = (
|
|||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
): void => {
|
): void => {
|
||||||
if (!isArrowElement(linearElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutateElement(linearElement, {
|
mutateElement(linearElement, {
|
||||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: {
|
[startOrEnd === "start" ? "startBinding" : "endBinding"]: {
|
||||||
elementId: hoveredElement.id,
|
elementId: hoveredElement.id,
|
||||||
@@ -448,13 +431,13 @@ export const getHoveredElementForBinding = (
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
},
|
},
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
const hoveredElement = getElementAtPosition(
|
const hoveredElement = getElementAtPosition(
|
||||||
[...elementsMap].map(([_, value]) => value),
|
app.scene.getNonDeletedElements(),
|
||||||
(element) =>
|
(element) =>
|
||||||
isBindableElement(element, false) &&
|
isBindableElement(element, false) &&
|
||||||
bindingBorderTest(element, pointerCoords, elementsMap),
|
bindingBorderTest(element, pointerCoords, app),
|
||||||
);
|
);
|
||||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
};
|
};
|
||||||
@@ -678,11 +661,15 @@ const maybeCalculateNewGapWhenScaling = (
|
|||||||
const getElligibleElementForBindingElement = (
|
const getElligibleElementForBindingElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
app: AppClassProperties,
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
return getHoveredElementForBinding(
|
return getHoveredElementForBinding(
|
||||||
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
getLinearElementEdgeCoors(
|
||||||
elementsMap,
|
linearElement,
|
||||||
|
startOrEnd,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
),
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -719,9 +706,6 @@ export const fixBindingsAfterDuplication = (
|
|||||||
const allBoundElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
const allBoundElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
|
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
|
||||||
const duplicateIdToOldId = new Map(
|
|
||||||
[...oldIdToDuplicatedId].map(([key, value]) => [value, key]),
|
|
||||||
);
|
|
||||||
oldElements.forEach((oldElement) => {
|
oldElements.forEach((oldElement) => {
|
||||||
const { boundElements } = oldElement;
|
const { boundElements } = oldElement;
|
||||||
if (boundElements != null && boundElements.length > 0) {
|
if (boundElements != null && boundElements.length > 0) {
|
||||||
@@ -771,11 +755,7 @@ export const fixBindingsAfterDuplication = (
|
|||||||
sceneElements
|
sceneElements
|
||||||
.filter(({ id }) => allBindableElementIds.has(id))
|
.filter(({ id }) => allBindableElementIds.has(id))
|
||||||
.forEach((bindableElement) => {
|
.forEach((bindableElement) => {
|
||||||
const oldElementId = duplicateIdToOldId.get(bindableElement.id);
|
const { boundElements } = bindableElement;
|
||||||
const { boundElements } = sceneElements.find(
|
|
||||||
({ id }) => id === oldElementId,
|
|
||||||
)!;
|
|
||||||
|
|
||||||
if (boundElements != null && boundElements.length > 0) {
|
if (boundElements != null && boundElements.length > 0) {
|
||||||
mutateElement(bindableElement, {
|
mutateElement(bindableElement, {
|
||||||
boundElements: boundElements.map((boundElement) =>
|
boundElements: boundElements.map((boundElement) =>
|
||||||
@@ -846,10 +826,10 @@ const newBoundElements = (
|
|||||||
const bindingBorderTest = (
|
const bindingBorderTest = (
|
||||||
element: NonDeleted<ExcalidrawBindableElement>,
|
element: NonDeleted<ExcalidrawBindableElement>,
|
||||||
{ x, y }: { x: number; y: number },
|
{ x, y }: { x: number; y: number },
|
||||||
elementsMap: ElementsMap,
|
app: AppClassProperties,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = maxBindingGap(element, element.width, element.height);
|
const threshold = maxBindingGap(element, element.width, element.height);
|
||||||
const shape = getElementShape(element, elementsMap);
|
const shape = app.getElementShape(element);
|
||||||
return isPointOnShape([x, y], shape, threshold);
|
return isPointOnShape([x, y], shape, threshold);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,11 @@ import { getCommonBounds } from "./bounds";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import type { NonDeletedExcalidrawElement } from "./types";
|
import type { NonDeletedExcalidrawElement } from "./types";
|
||||||
import type { AppState, NormalizedZoomValue, PointerDownState } from "../types";
|
import type { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElement, getMinTextElementWidth } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { getGridPoint } from "../math";
|
import { getGridPoint } from "../math";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import {
|
import { isArrowElement, isFrameLikeElement } from "./typeChecks";
|
||||||
isArrowElement,
|
|
||||||
isFrameLikeElement,
|
|
||||||
isTextElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import { getFontString } from "../utils";
|
|
||||||
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -146,7 +140,6 @@ export const dragNewElement = (
|
|||||||
height: number,
|
height: number,
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
zoom: NormalizedZoomValue,
|
|
||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null,
|
widthAspectRatio?: number | null,
|
||||||
@@ -192,41 +185,12 @@ export const dragNewElement = (
|
|||||||
newY = originY - height / 2;
|
newY = originY - height / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
let textAutoResize = null;
|
|
||||||
|
|
||||||
// NOTE this should apply only to creating text elements, not existing
|
|
||||||
// (once we rewrite appState.draggingElement to actually mean dragging
|
|
||||||
// elements)
|
|
||||||
if (isTextElement(draggingElement)) {
|
|
||||||
height = draggingElement.height;
|
|
||||||
const minWidth = getMinTextElementWidth(
|
|
||||||
getFontString({
|
|
||||||
fontSize: draggingElement.fontSize,
|
|
||||||
fontFamily: draggingElement.fontFamily,
|
|
||||||
}),
|
|
||||||
draggingElement.lineHeight,
|
|
||||||
);
|
|
||||||
width = Math.max(width, minWidth);
|
|
||||||
|
|
||||||
if (Math.abs(x - originX) > TEXT_AUTOWRAP_THRESHOLD / zoom) {
|
|
||||||
textAutoResize = {
|
|
||||||
autoResize: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
newY = originY;
|
|
||||||
if (shouldResizeFromCenter) {
|
|
||||||
newX = originX - width / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width !== 0 && height !== 0) {
|
if (width !== 0 && height !== 0) {
|
||||||
mutateElement(draggingElement, {
|
mutateElement(draggingElement, {
|
||||||
x: newX + (originOffset?.x ?? 0),
|
x: newX + (originOffset?.x ?? 0),
|
||||||
y: newY + (originOffset?.y ?? 0),
|
y: newY + (originOffset?.y ?? 0),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
...textAutoResize,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elementsMap,
|
app,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -715,10 +715,7 @@ export class LinearElementEditor {
|
|||||||
},
|
},
|
||||||
selectedPointsIndices: [element.points.length - 1],
|
selectedPointsIndices: [element.points.length - 1],
|
||||||
lastUncommittedPoint: null,
|
lastUncommittedPoint: null,
|
||||||
endBindingElement: getHoveredElementForBinding(
|
endBindingElement: getHoveredElementForBinding(scenePointer, app),
|
||||||
scenePointer,
|
|
||||||
elementsMap,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ret.didAddPoint = true;
|
ret.didAddPoint = true;
|
||||||
@@ -1168,7 +1165,7 @@ export class LinearElementEditor {
|
|||||||
const nextPoints = points.map((point, idx) => {
|
const nextPoints = points.map((point, idx) => {
|
||||||
const selectedPointData = targetPoints.find((p) => p.index === idx);
|
const selectedPointData = targetPoints.find((p) => p.index === idx);
|
||||||
if (selectedPointData) {
|
if (selectedPointData) {
|
||||||
if (selectedPointData.index === 0) {
|
if (selectedOriginPoint) {
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,10 +1174,7 @@ export class LinearElementEditor {
|
|||||||
const deltaY =
|
const deltaY =
|
||||||
selectedPointData.point[1] - points[selectedPointData.index][1];
|
selectedPointData.point[1] - points[selectedPointData.index][1];
|
||||||
|
|
||||||
return [
|
return [point[0] + deltaX, point[1] + deltaY] as const;
|
||||||
point[0] + deltaX - offsetX,
|
|
||||||
point[1] + deltaY - offsetY,
|
|
||||||
] as const;
|
|
||||||
}
|
}
|
||||||
return offsetX || offsetY
|
return offsetX || offsetY
|
||||||
? ([point[0] - offsetX, point[1] - offsetY] as const)
|
? ([point[0] - offsetX, point[1] - offsetY] as const)
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
export const newElementWith = <TElement extends ExcalidrawElement>(
|
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
|
/** pass `true` to always regenerate */
|
||||||
|
force = false,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
@@ -123,7 +125,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!didChange) {
|
if (!didChange && !force) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ const getTextElementPositionOffsets = (
|
|||||||
export const newTextElement = (
|
export const newTextElement = (
|
||||||
opts: {
|
opts: {
|
||||||
text: string;
|
text: string;
|
||||||
originalText?: string;
|
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
fontFamily?: FontFamilyValues;
|
fontFamily?: FontFamilyValues;
|
||||||
textAlign?: TextAlign;
|
textAlign?: TextAlign;
|
||||||
@@ -223,7 +222,6 @@ export const newTextElement = (
|
|||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||||
autoResize?: ExcalidrawTextElement["autoResize"];
|
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
@@ -254,8 +252,8 @@ export const newTextElement = (
|
|||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: opts.originalText ?? text,
|
originalText: text,
|
||||||
autoResize: opts.autoResize ?? true,
|
autoResize: true,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
MIN_FONT_SIZE,
|
||||||
|
SHIFT_LOCKING_ANGLE,
|
||||||
|
} from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, centerPoint, rotatePoint } from "../math";
|
import { rotate, centerPoint, rotatePoint } from "../math";
|
||||||
@@ -47,7 +51,7 @@ import {
|
|||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
wrapText,
|
wrapText,
|
||||||
measureText,
|
measureText,
|
||||||
getMinTextElementWidth,
|
getMinCharWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { isInGroup } from "../groups";
|
import { isInGroup } from "../groups";
|
||||||
@@ -178,7 +182,7 @@ const rotateSingleElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rescalePointsInElement = (
|
const rescalePointsInElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
@@ -195,7 +199,7 @@ export const rescalePointsInElement = (
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
export const measureFontSizeFromWidth = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
@@ -348,13 +352,8 @@ const resizeSingleTextElement = (
|
|||||||
const boundsCurrentWidth = esx2 - esx1;
|
const boundsCurrentWidth = esx2 - esx1;
|
||||||
|
|
||||||
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
|
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
|
||||||
const minWidth = getMinTextElementWidth(
|
const minWidth =
|
||||||
getFontString({
|
getMinCharWidth(getFontString(element)) + BOUND_TEXT_PADDING * 2;
|
||||||
fontSize: element.fontSize,
|
|
||||||
fontFamily: element.fontFamily,
|
|
||||||
}),
|
|
||||||
element.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
|
|
||||||
|
|||||||
@@ -938,10 +938,3 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
|||||||
}
|
}
|
||||||
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMinTextElementWidth = (
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("text should never go beyond max width", async () => {
|
it("text should never go beyond max width", async () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.click(0, 0);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = await getTextEditor(textEditorSelector, true);
|
textarea = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
@@ -1064,7 +1064,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
85,
|
85,
|
||||||
"5.00000",
|
4.999999999999986,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -1109,8 +1109,8 @@ describe("textWysiwyg", () => {
|
|||||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"375.00000",
|
374.99999999999994,
|
||||||
"-535.00000",
|
-535.0000000000001,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export const textWysiwyg = ({
|
|||||||
canvas,
|
canvas,
|
||||||
excalidrawContainer,
|
excalidrawContainer,
|
||||||
app,
|
app,
|
||||||
autoSelect = true,
|
|
||||||
}: {
|
}: {
|
||||||
id: ExcalidrawElement["id"];
|
id: ExcalidrawElement["id"];
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +92,6 @@ export const textWysiwyg = ({
|
|||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
app: App;
|
app: App;
|
||||||
autoSelect?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const textPropertiesUpdated = (
|
const textPropertiesUpdated = (
|
||||||
updatedTextElement: ExcalidrawTextElement,
|
updatedTextElement: ExcalidrawTextElement,
|
||||||
@@ -493,11 +491,6 @@ export const textWysiwyg = ({
|
|||||||
// so that we don't need to create separate a callback for event handlers
|
// so that we don't need to create separate a callback for event handlers
|
||||||
let submittedViaKeyboard = false;
|
let submittedViaKeyboard = false;
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
// prevent double submit
|
|
||||||
if (isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isDestroyed = true;
|
|
||||||
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
||||||
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||||
// wysiwyg on update
|
// wysiwyg on update
|
||||||
@@ -551,6 +544,10 @@ export const textWysiwyg = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
if (isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isDestroyed = true;
|
||||||
// remove events to ensure they don't late-fire
|
// remove events to ensure they don't late-fire
|
||||||
editable.onblur = null;
|
editable.onblur = null;
|
||||||
editable.oninput = null;
|
editable.oninput = null;
|
||||||
@@ -642,22 +639,6 @@ export const textWysiwyg = ({
|
|||||||
// handle edge-case where pointerup doesn't fire e.g. due to user
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
||||||
// alt-tabbing away
|
// alt-tabbing away
|
||||||
window.addEventListener("blur", handleSubmit);
|
window.addEventListener("blur", handleSubmit);
|
||||||
} else if (
|
|
||||||
event.target instanceof HTMLElement &&
|
|
||||||
!event.target.contains(editable) &&
|
|
||||||
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
|
||||||
// so without introducing crazier hacks, nothing we can do
|
|
||||||
!isTestEnv()
|
|
||||||
) {
|
|
||||||
// On mobile, blur event doesn't seem to always fire correctly,
|
|
||||||
// so we want to also submit on pointerdown outside the wysiwyg.
|
|
||||||
// Done in the next frame to prevent pointerdown from creating a new text
|
|
||||||
// immediately (if tools locked) so that users on mobile have chance
|
|
||||||
// to submit first (to hide virtual keyboard).
|
|
||||||
// Note: revisit if we want to differ this behavior on Desktop
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
handleSubmit();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -676,11 +657,9 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
|
|
||||||
if (autoSelect) {
|
// select on init (focusing is done separately inside the bindBlurEvent()
|
||||||
// select on init (focusing is done separately inside the bindBlurEvent()
|
// because we need it to happen *after* the blur event from `pointerdown`)
|
||||||
// because we need it to happen *after* the blur event from `pointerdown`)
|
editable.select();
|
||||||
editable.select();
|
|
||||||
}
|
|
||||||
bindBlurEvent();
|
bindBlurEvent();
|
||||||
|
|
||||||
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
||||||
@@ -695,13 +674,7 @@ export const textWysiwyg = ({
|
|||||||
window.addEventListener("resize", updateWysiwygStyle);
|
window.addEventListener("resize", updateWysiwygStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
editable.onpointerdown = (event) => event.stopPropagation();
|
window.addEventListener("pointerdown", onPointerDown);
|
||||||
|
|
||||||
// rAF (+ capture to by doubly sure) so we don't catch te pointerdown that
|
|
||||||
// triggered the wysiwyg
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
|
||||||
});
|
|
||||||
window.addEventListener("wheel", stopEvent, {
|
window.addEventListener("wheel", stopEvent, {
|
||||||
passive: false,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export const isBindingElementType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isBindableElement = (
|
export const isBindableElement = (
|
||||||
element: ExcalidrawElement | null | undefined,
|
element: ExcalidrawElement | null,
|
||||||
includeLocked = true,
|
includeLocked = true,
|
||||||
): element is ExcalidrawBindableElement => {
|
): element is ExcalidrawBindableElement => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -133,11 +133,27 @@ const getMovedIndicesGroups = (
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < elements.length) {
|
while (i < elements.length) {
|
||||||
if (movedElements.has(elements[i].id)) {
|
if (
|
||||||
|
movedElements.has(elements[i].id) &&
|
||||||
|
!isValidFractionalIndex(
|
||||||
|
elements[i]?.index,
|
||||||
|
elements[i - 1]?.index,
|
||||||
|
elements[i + 1]?.index,
|
||||||
|
)
|
||||||
|
) {
|
||||||
const indicesGroup = [i - 1, i]; // push the lower bound index as the first item
|
const indicesGroup = [i - 1, i]; // push the lower bound index as the first item
|
||||||
|
|
||||||
while (++i < elements.length) {
|
while (++i < elements.length) {
|
||||||
if (!movedElements.has(elements[i].id)) {
|
if (
|
||||||
|
!(
|
||||||
|
movedElements.has(elements[i].id) &&
|
||||||
|
!isValidFractionalIndex(
|
||||||
|
elements[i]?.index,
|
||||||
|
elements[i - 1]?.index,
|
||||||
|
elements[i + 1]?.index,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -373,9 +373,7 @@ export const getNonDeletedGroupIds = (elements: ElementsMap) => {
|
|||||||
return nonDeletedGroupIds;
|
return nonDeletedGroupIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const elementsAreInSameGroup = (
|
export const elementsAreInSameGroup = (elements: ExcalidrawElement[]) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
const allGroups = elements.flatMap((element) => element.groupIds);
|
const allGroups = elements.flatMap((element) => element.groupIds);
|
||||||
const groupCount = new Map<string, number>();
|
const groupCount = new Map<string, number>();
|
||||||
let maxGroup = 0;
|
let maxGroup = 0;
|
||||||
|
|||||||
@@ -270,22 +270,6 @@
|
|||||||
"mermaidToExcalidraw": "Mermaid to Excalidraw",
|
"mermaidToExcalidraw": "Mermaid to Excalidraw",
|
||||||
"magicSettings": "AI settings"
|
"magicSettings": "AI settings"
|
||||||
},
|
},
|
||||||
"element": {
|
|
||||||
"rectangle": "Rectangle",
|
|
||||||
"diamond": "Diamond",
|
|
||||||
"ellipse": "Ellipse",
|
|
||||||
"arrow": "Arrow",
|
|
||||||
"line": "Line",
|
|
||||||
"freedraw": "Freedraw",
|
|
||||||
"text": "Text",
|
|
||||||
"image": "Image",
|
|
||||||
"group": "Group",
|
|
||||||
"frame": "Frame",
|
|
||||||
"magicframe": "Wireframe to code",
|
|
||||||
"embeddable": "Web Embed",
|
|
||||||
"selection": "Selection",
|
|
||||||
"iframe": "IFrame"
|
|
||||||
},
|
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
"selectedShapeActions": "Selected shape actions",
|
"selectedShapeActions": "Selected shape actions",
|
||||||
@@ -459,10 +443,7 @@
|
|||||||
"scene": "Scene",
|
"scene": "Scene",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
"fullTitle": "Stats & Element properties",
|
"title": "Stats for nerds",
|
||||||
"title": "Stats",
|
|
||||||
"generalStats": "General stats",
|
|
||||||
"elementProperties": "Element properties",
|
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"versionCopy": "Click to copy",
|
"versionCopy": "Click to copy",
|
||||||
|
|||||||
@@ -475,14 +475,6 @@ export const isRightAngle = (angle: number) => {
|
|||||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const radianToDegree = (r: number) => {
|
|
||||||
return (r * 180) / Math.PI;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const degreeToRadian = (d: number) => {
|
|
||||||
return (d / 180) * Math.PI;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Given two ranges, return if the two ranges overlap with each other
|
// Given two ranges, return if the two ranges overlap with each other
|
||||||
// e.g. [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5]
|
// e.g. [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5]
|
||||||
export const rangesOverlap = (
|
export const rangesOverlap = (
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/** heuristically checks whether the text may be a mermaid diagram definition */
|
|
||||||
export const isMaybeMermaidDefinition = (text: string) => {
|
|
||||||
const chartTypes = [
|
|
||||||
"flowchart",
|
|
||||||
"sequenceDiagram",
|
|
||||||
"classDiagram",
|
|
||||||
"stateDiagram",
|
|
||||||
"stateDiagram-v2",
|
|
||||||
"erDiagram",
|
|
||||||
"journey",
|
|
||||||
"gantt",
|
|
||||||
"pie",
|
|
||||||
"quadrantChart",
|
|
||||||
"requirementDiagram",
|
|
||||||
"gitGraph",
|
|
||||||
"C4Context",
|
|
||||||
"mindmap",
|
|
||||||
"timeline",
|
|
||||||
"zenuml",
|
|
||||||
"sankey",
|
|
||||||
"xychart",
|
|
||||||
"block",
|
|
||||||
];
|
|
||||||
|
|
||||||
const re = new RegExp(
|
|
||||||
`^(?:%%{.*?}%%[\\s\\n]*)?\\b${chartTypes
|
|
||||||
.map((x) => `${x}(-beta)?`)
|
|
||||||
.join("|")}\\b`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return re.test(text.trim());
|
|
||||||
};
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "1.1.0",
|
"@excalidraw/mermaid-to-excalidraw": "1.0.0",
|
||||||
"@excalidraw/random-username": "1.1.0",
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@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",
|
||||||
@@ -72,7 +72,6 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.13.1",
|
"jotai": "1.13.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"lz-string": "1.5.0",
|
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
|||||||
@@ -47,18 +47,13 @@ import {
|
|||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import {
|
import { isFrameLikeElement, isLinearElement } from "../element/typeChecks";
|
||||||
isFrameLikeElement,
|
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
|
||||||
GroupId,
|
GroupId,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -308,6 +303,7 @@ const renderSelectionBorder = (
|
|||||||
cy: number;
|
cy: number;
|
||||||
activeEmbeddable: boolean;
|
activeEmbeddable: boolean;
|
||||||
},
|
},
|
||||||
|
padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
angle,
|
angle,
|
||||||
@@ -324,8 +320,6 @@ const renderSelectionBorder = (
|
|||||||
const elementWidth = elementX2 - elementX1;
|
const elementWidth = elementX2 - elementX1;
|
||||||
const elementHeight = elementY2 - elementY1;
|
const elementHeight = elementY2 - elementY1;
|
||||||
|
|
||||||
const padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2;
|
|
||||||
|
|
||||||
const linePadding = padding / appState.zoom.value;
|
const linePadding = padding / appState.zoom.value;
|
||||||
const lineWidth = 8 / appState.zoom.value;
|
const lineWidth = 8 / appState.zoom.value;
|
||||||
const spaceWidth = 4 / appState.zoom.value;
|
const spaceWidth = 4 / appState.zoom.value;
|
||||||
@@ -576,34 +570,11 @@ const renderTransformHandles = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTextBox = (
|
|
||||||
text: NonDeleted<ExcalidrawTextElement>,
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
appState: InteractiveCanvasAppState,
|
|
||||||
selectionColor: InteractiveCanvasRenderConfig["selectionColor"],
|
|
||||||
) => {
|
|
||||||
context.save();
|
|
||||||
const padding = (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
|
||||||
const width = text.width + padding * 2;
|
|
||||||
const height = text.height + padding * 2;
|
|
||||||
const cx = text.x + width / 2;
|
|
||||||
const cy = text.y + height / 2;
|
|
||||||
const shiftX = -(width / 2 + padding);
|
|
||||||
const shiftY = -(height / 2 + padding);
|
|
||||||
context.translate(cx + appState.scrollX, cy + appState.scrollY);
|
|
||||||
context.rotate(text.angle);
|
|
||||||
context.lineWidth = 1 / appState.zoom.value;
|
|
||||||
context.strokeStyle = selectionColor;
|
|
||||||
context.strokeRect(shiftX, shiftY, width, height);
|
|
||||||
context.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
const _renderInteractiveScene = ({
|
const _renderInteractiveScene = ({
|
||||||
canvas,
|
canvas,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
allElementsMap,
|
|
||||||
scale,
|
scale,
|
||||||
appState,
|
appState,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
@@ -655,31 +626,12 @@ const _renderInteractiveScene = ({
|
|||||||
// Paint selection element
|
// Paint selection element
|
||||||
if (appState.selectionElement) {
|
if (appState.selectionElement) {
|
||||||
try {
|
try {
|
||||||
renderSelectionElement(
|
renderSelectionElement(appState.selectionElement, context, appState);
|
||||||
appState.selectionElement,
|
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
renderConfig.selectionColor,
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.editingElement && isTextElement(appState.editingElement)) {
|
|
||||||
const textElement = allElementsMap.get(appState.editingElement.id) as
|
|
||||||
| ExcalidrawTextElement
|
|
||||||
| undefined;
|
|
||||||
if (textElement && !textElement.autoResize) {
|
|
||||||
renderTextBox(
|
|
||||||
textElement,
|
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
renderConfig.selectionColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appState.isBindingEnabled) {
|
if (appState.isBindingEnabled) {
|
||||||
appState.suggestedBindings
|
appState.suggestedBindings
|
||||||
.filter((binding) => binding != null)
|
.filter((binding) => binding != null)
|
||||||
@@ -858,12 +810,7 @@ const _renderInteractiveScene = ({
|
|||||||
"mouse", // when we render we don't know which pointer type so use mouse,
|
"mouse", // when we render we don't know which pointer type so use mouse,
|
||||||
getOmitSidesForDevice(device),
|
getOmitSidesForDevice(device),
|
||||||
);
|
);
|
||||||
if (
|
if (!appState.viewModeEnabled && showBoundingBox) {
|
||||||
!appState.viewModeEnabled &&
|
|
||||||
showBoundingBox &&
|
|
||||||
// do not show transform handles when text is being edited
|
|
||||||
!isTextElement(appState.editingElement)
|
|
||||||
) {
|
|
||||||
renderTransformHandles(
|
renderTransformHandles(
|
||||||
context,
|
context,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import type {
|
import type {
|
||||||
StaticCanvasRenderConfig,
|
StaticCanvasRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
InteractiveCanvasRenderConfig,
|
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import { distance, getFontString, isRTL } from "../utils";
|
import { distance, getFontString, isRTL } from "../utils";
|
||||||
import { getCornerRadius, isRightAngle } from "../math";
|
import { getCornerRadius, isRightAngle } from "../math";
|
||||||
@@ -90,7 +89,7 @@ const shouldResetImageFilter = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCanvasPadding = (element: ExcalidrawElement) =>
|
const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||||
element.type === "freedraw" ? element.strokeWidth * 12 : 200;
|
element.type === "freedraw" ? element.strokeWidth * 12 : 20;
|
||||||
|
|
||||||
export const getRenderOpacity = (
|
export const getRenderOpacity = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@@ -471,7 +470,16 @@ const drawElementFromCanvas = (
|
|||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
const padding = getCanvasPadding(element);
|
const padding = getCanvasPadding(element);
|
||||||
const zoom = elementWithCanvas.scale;
|
const zoom = elementWithCanvas.scale;
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, allElementsMap);
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element, allElementsMap);
|
||||||
|
|
||||||
|
// Free draw elements will otherwise "shuffle" as the min x and y change
|
||||||
|
if (isFreeDrawElement(element)) {
|
||||||
|
x1 = Math.floor(x1);
|
||||||
|
x2 = Math.ceil(x2);
|
||||||
|
y1 = Math.floor(y1);
|
||||||
|
y2 = Math.ceil(y2);
|
||||||
|
}
|
||||||
|
|
||||||
const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio;
|
const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio;
|
||||||
const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio;
|
const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio;
|
||||||
|
|
||||||
@@ -610,7 +618,6 @@ export const renderSelectionElement = (
|
|||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
selectionColor: InteractiveCanvasRenderConfig["selectionColor"],
|
|
||||||
) => {
|
) => {
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
|
context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
|
||||||
@@ -624,7 +631,7 @@ export const renderSelectionElement = (
|
|||||||
|
|
||||||
context.fillRect(offset, offset, element.width, element.height);
|
context.fillRect(offset, offset, element.width, element.height);
|
||||||
context.lineWidth = 1 / appState.zoom.value;
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
context.strokeStyle = selectionColor;
|
context.strokeStyle = " rgb(105, 101, 219)";
|
||||||
context.strokeRect(offset, offset, element.width, element.height);
|
context.strokeRect(offset, offset, element.width, element.height);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { getContainerElement } from "../element/textElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -46,18 +46,14 @@ export class Fonts {
|
|||||||
|
|
||||||
let didUpdate = false;
|
let didUpdate = false;
|
||||||
|
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
this.scene.mapElements((element) => {
|
||||||
|
|
||||||
for (const element of this.scene.getNonDeletedElements()) {
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
const container = getContainerElement(element, elementsMap);
|
return newElementWith(element, {}, true);
|
||||||
if (container) {
|
|
||||||
ShapeCache.delete(container);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
if (didUpdate) {
|
if (didUpdate) {
|
||||||
this.scene.triggerUpdate();
|
this.scene.triggerUpdate();
|
||||||
|
|||||||
@@ -105,9 +105,6 @@ class Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated pass down `app.scene` and use it directly
|
|
||||||
*/
|
|
||||||
static getScene(elementKey: ElementKey): Scene | null {
|
static getScene(elementKey: ElementKey): Scene | null {
|
||||||
if (isIdKey(elementKey)) {
|
if (isIdKey(elementKey)) {
|
||||||
return this.sceneMapById.get(elementKey) || null;
|
return this.sceneMapById.get(elementKey) || null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import type { Drawable } from "roughjs/bin/core";
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
NonDeletedElementsMap,
|
NonDeletedElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
@@ -54,7 +55,7 @@ export type InteractiveCanvasRenderConfig = {
|
|||||||
remotePointerUserStates: Map<SocketId, UserIdleState>;
|
remotePointerUserStates: Map<SocketId, UserIdleState>;
|
||||||
remotePointerUsernames: Map<SocketId, string>;
|
remotePointerUsernames: Map<SocketId, string>;
|
||||||
remotePointerButton: Map<SocketId, string | undefined>;
|
remotePointerButton: Map<SocketId, string | undefined>;
|
||||||
selectionColor: string;
|
selectionColor?: string;
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
renderScrollbars?: boolean;
|
renderScrollbars?: boolean;
|
||||||
@@ -82,7 +83,6 @@ export type InteractiveSceneRenderConfig = {
|
|||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
allElementsMap: NonDeletedSceneElementsMap;
|
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
renderConfig: InteractiveCanvasRenderConfig;
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
@@ -95,6 +95,10 @@ export type SceneScroll = {
|
|||||||
scrollY: number;
|
scrollY: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Scene {
|
||||||
|
elements: ExcalidrawTextElement[];
|
||||||
|
}
|
||||||
|
|
||||||
export type ExportType =
|
export type ExportType =
|
||||||
| "png"
|
| "png"
|
||||||
| "clipboard"
|
| "clipboard"
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
import {
|
|
||||||
getClosedCurveShape,
|
|
||||||
getCurveShape,
|
|
||||||
getEllipseShape,
|
|
||||||
getFreedrawShape,
|
|
||||||
getPolygonShape,
|
|
||||||
type GeometricShape,
|
|
||||||
} from "../utils/geometry/shape";
|
|
||||||
import {
|
import {
|
||||||
ArrowIcon,
|
ArrowIcon,
|
||||||
DiamondIcon,
|
DiamondIcon,
|
||||||
@@ -18,11 +10,7 @@ import {
|
|||||||
SelectionIcon,
|
SelectionIcon,
|
||||||
TextIcon,
|
TextIcon,
|
||||||
} from "./components/icons";
|
} from "./components/icons";
|
||||||
import { getElementAbsoluteCoords } from "./element";
|
|
||||||
import { shouldTestInside } from "./element/collision";
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
|
||||||
import { KEYS } from "./keys";
|
import { KEYS } from "./keys";
|
||||||
import { ShapeCache } from "./scene/ShapeCache";
|
|
||||||
|
|
||||||
export const SHAPES = [
|
export const SHAPES = [
|
||||||
{
|
{
|
||||||
@@ -109,53 +97,3 @@ export const findShapeByKey = (key: string) => {
|
|||||||
});
|
});
|
||||||
return shape?.value || null;
|
return shape?.value || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* get the pure geometric shape of an excalidraw element
|
|
||||||
* which is then used for hit detection
|
|
||||||
*/
|
|
||||||
export const getElementShape = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): GeometricShape => {
|
|
||||||
switch (element.type) {
|
|
||||||
case "rectangle":
|
|
||||||
case "diamond":
|
|
||||||
case "frame":
|
|
||||||
case "magicframe":
|
|
||||||
case "embeddable":
|
|
||||||
case "image":
|
|
||||||
case "iframe":
|
|
||||||
case "text":
|
|
||||||
case "selection":
|
|
||||||
return getPolygonShape(element);
|
|
||||||
case "arrow":
|
|
||||||
case "line": {
|
|
||||||
const roughShape =
|
|
||||||
ShapeCache.get(element)?.[0] ??
|
|
||||||
ShapeCache.generateElementShape(element, null)[0];
|
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
|
||||||
|
|
||||||
return shouldTestInside(element)
|
|
||||||
? getClosedCurveShape(
|
|
||||||
element,
|
|
||||||
roughShape,
|
|
||||||
[element.x, element.y],
|
|
||||||
element.angle,
|
|
||||||
[cx, cy],
|
|
||||||
)
|
|
||||||
: getCurveShape(roughShape, [element.x, element.y], element.angle, [
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ellipse":
|
|
||||||
return getEllipseShape(element);
|
|
||||||
|
|
||||||
case "freedraw": {
|
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
|
||||||
return getFreedrawShape(element, [cx, cy], shouldTestInside(element));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1375,7 +1375,6 @@ export const isActiveToolNonLinearSnappable = (
|
|||||||
activeToolType === TOOL_TYPE.diamond ||
|
activeToolType === TOOL_TYPE.diamond ||
|
||||||
activeToolType === TOOL_TYPE.frame ||
|
activeToolType === TOOL_TYPE.frame ||
|
||||||
activeToolType === TOOL_TYPE.magicframe ||
|
activeToolType === TOOL_TYPE.magicframe ||
|
||||||
activeToolType === TOOL_TYPE.image ||
|
activeToolType === TOOL_TYPE.image
|
||||||
activeToolType === TOOL_TYPE.text
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { deepCopyElement } from "./element/newElement";
|
|||||||
import type { OrderedExcalidrawElement } from "./element/types";
|
import type { OrderedExcalidrawElement } from "./element/types";
|
||||||
import { Emitter } from "./emitter";
|
import { Emitter } from "./emitter";
|
||||||
import type { AppState, ObservedAppState } from "./types";
|
import type { AppState, ObservedAppState } from "./types";
|
||||||
import type { ValueOf } from "./utility-types";
|
|
||||||
import { isShallowEqual } from "./utils";
|
import { isShallowEqual } from "./utils";
|
||||||
|
|
||||||
// hidden non-enumerable property for runtime checks
|
// hidden non-enumerable property for runtime checks
|
||||||
@@ -36,41 +35,16 @@ const isObservedAppState = (
|
|||||||
): appState is ObservedAppState =>
|
): appState is ObservedAppState =>
|
||||||
!!Reflect.get(appState, hiddenObservedAppStateProp);
|
!!Reflect.get(appState, hiddenObservedAppStateProp);
|
||||||
|
|
||||||
export const StoreAction = {
|
export type StoreActionType = "capture" | "update" | "none";
|
||||||
/**
|
|
||||||
* Immediately undoable.
|
export const StoreAction: {
|
||||||
*
|
[K in Uppercase<StoreActionType>]: StoreActionType;
|
||||||
* Use for updates which should be captured.
|
} = {
|
||||||
* Should be used for most of the local updates.
|
|
||||||
*
|
|
||||||
* These updates will _immediately_ make it to the local undo / redo stacks.
|
|
||||||
*/
|
|
||||||
CAPTURE: "capture",
|
CAPTURE: "capture",
|
||||||
/**
|
|
||||||
* Never undoable.
|
|
||||||
*
|
|
||||||
* Use for updates which should never be recorded, such as remote updates
|
|
||||||
* or scene initialization.
|
|
||||||
*
|
|
||||||
* These updates will _never_ make it to the local undo / redo stacks.
|
|
||||||
*/
|
|
||||||
UPDATE: "update",
|
UPDATE: "update",
|
||||||
/**
|
|
||||||
* Eventually undoable.
|
|
||||||
*
|
|
||||||
* Use for updates which should not be captured immediately - likely
|
|
||||||
* exceptions which are part of some async multi-step process. Otherwise, all
|
|
||||||
* such updates would end up being captured with the next
|
|
||||||
* `StoreAction.CAPTURE` - triggered either by the next `updateScene`
|
|
||||||
* or internally by the editor.
|
|
||||||
*
|
|
||||||
* These updates will _eventually_ make it to the local undo / redo stacks.
|
|
||||||
*/
|
|
||||||
NONE: "none",
|
NONE: "none",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type StoreActionType = ValueOf<typeof StoreAction>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent an increment to the Store.
|
* Represent an increment to the Store.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { act, render, waitFor } from "./test-utils";
|
import { act, render, waitFor } from "./test-utils";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { expect } from "vitest";
|
import React from "react";
|
||||||
|
import { expect, vi } from "vitest";
|
||||||
|
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
|
||||||
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
||||||
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
|
||||||
|
|
||||||
mockMermaidToExcalidraw({
|
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
|
||||||
mockRef: true,
|
const module = (await importActual()) as any;
|
||||||
parseMermaidToExcalidraw: async (definition) => {
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...module,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const parseMermaidToExcalidrawSpy = vi.spyOn(
|
||||||
|
MermaidToExcalidraw,
|
||||||
|
"parseMermaidToExcalidraw",
|
||||||
|
);
|
||||||
|
|
||||||
|
parseMermaidToExcalidrawSpy.mockImplementation(
|
||||||
|
async (
|
||||||
|
definition: string,
|
||||||
|
options?: MermaidToExcalidraw.MermaidOptions | undefined,
|
||||||
|
) => {
|
||||||
const firstLine = definition.split("\n")[0];
|
const firstLine = definition.split("\n")[0];
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (firstLine === "flowchart TD") {
|
if (firstLine === "flowchart TD") {
|
||||||
@@ -72,6 +88,12 @@ mockMermaidToExcalidraw({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(React, "useRef").mockReturnValue({
|
||||||
|
current: {
|
||||||
|
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test <MermaidToExcalidraw/>", () => {
|
describe("Test <MermaidToExcalidraw/>", () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -130,7 +130,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -182,7 +182,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -248,7 +248,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -288,7 +288,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -406,7 +406,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -453,7 +453,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -505,7 +505,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -554,7 +554,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -603,7 +603,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -643,7 +643,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -874,13 +874,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1069,13 +1066,10 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -1280,13 +1274,10 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1606,13 +1597,10 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1932,13 +1920,10 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -2141,13 +2126,10 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2378,13 +2360,10 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2679,13 +2658,10 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3038,13 +3014,10 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
@@ -3508,13 +3481,10 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3826,13 +3796,10 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -4147,13 +4114,10 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -4484,7 +4448,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4587,7 +4551,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4639,7 +4603,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4705,7 +4669,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4745,7 +4709,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4863,7 +4827,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4910,7 +4874,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -4962,7 +4926,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5011,7 +4975,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5060,7 +5024,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5100,7 +5064,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5328,13 +5292,10 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -5606,7 +5567,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5709,7 +5670,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5761,7 +5722,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5827,7 +5788,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5867,7 +5828,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -5985,7 +5946,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6032,7 +5993,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6084,7 +6045,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6133,7 +6094,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6182,7 +6143,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6222,7 +6183,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6452,13 +6413,10 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -6783,7 +6741,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -6835,7 +6793,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7047,7 +7005,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7103,7 +7061,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7147,7 +7105,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7200,7 +7158,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
<g
|
<g
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7242,7 +7200,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7285,12 +7243,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
</g>
|
</g>
|
||||||
</svg>,
|
</svg>,
|
||||||
"keyTest": [Function],
|
"keyTest": [Function],
|
||||||
"keywords": [
|
"label": "stats.title",
|
||||||
"edit",
|
|
||||||
"attributes",
|
|
||||||
"customize",
|
|
||||||
],
|
|
||||||
"label": "stats.fullTitle",
|
|
||||||
"name": "stats",
|
"name": "stats",
|
||||||
"paletteName": "Toggle stats",
|
"paletteName": "Toggle stats",
|
||||||
"perform": [Function],
|
"perform": [Function],
|
||||||
@@ -7378,13 +7331,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -7443,7 +7393,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7546,7 +7496,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7598,7 +7548,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7664,7 +7614,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7704,7 +7654,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7822,7 +7772,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7869,7 +7819,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7921,7 +7871,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -7970,7 +7920,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8019,7 +7969,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8059,7 +8009,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8284,13 +8234,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8332,7 +8279,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8435,7 +8382,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8487,7 +8434,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8553,7 +8500,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8593,7 +8540,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8711,7 +8658,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8758,7 +8705,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8810,7 +8757,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8859,7 +8806,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.50000"}
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8908,7 +8855,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -8948,7 +8895,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
strokeWidth={"1.25000"}
|
strokeWidth={1.25}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
@@ -9176,13 +9123,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -189,13 +189,13 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id1",
|
"elementId": "id1",
|
||||||
"focus": "-0.46667",
|
"focus": -0.46666666666666673,
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "81.48231",
|
"height": 81.48231043525051,
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -210,7 +210,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
81,
|
81,
|
||||||
"81.48231",
|
81.48231043525051,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -221,7 +221,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"focus": "-0.60000",
|
"focus": -0.6000000000000001,
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@@ -233,6 +233,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"versionNonce": 2066753033,
|
"versionNonce": 2066753033,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": "49.98179",
|
"y": 49.981789081137734,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -92,13 +92,10 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -339,7 +336,7 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id5",
|
"id5",
|
||||||
],
|
],
|
||||||
"index": "a2",
|
"index": "a1V",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
@@ -351,11 +348,9 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id5",
|
"id5",
|
||||||
],
|
],
|
||||||
"index": "a3",
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"index": "a2",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -501,13 +496,10 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -727,7 +719,7 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id4",
|
"id4",
|
||||||
],
|
],
|
||||||
"index": "a2",
|
"index": "a1V",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
@@ -739,11 +731,9 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id4",
|
"id4",
|
||||||
],
|
],
|
||||||
"index": "a3",
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"index": "a2",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -890,13 +880,10 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1427,13 +1414,10 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1628,13 +1612,10 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -1875,7 +1856,7 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id5",
|
"id5",
|
||||||
],
|
],
|
||||||
"index": "a2",
|
"index": "a1V",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
@@ -1887,11 +1868,9 @@ History {
|
|||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id5",
|
"id5",
|
||||||
],
|
],
|
||||||
"index": "a3",
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"index": "a2",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1992,13 +1971,10 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2222,13 +2198,10 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2396,13 +2369,10 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2706,13 +2676,10 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -2946,13 +2913,10 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3181,13 +3145,10 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3403,13 +3364,10 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3652,13 +3610,10 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -3954,13 +3909,10 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -4419,13 +4371,10 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
},
|
},
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -4694,13 +4643,10 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -4998,13 +4944,10 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
},
|
},
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -5170,13 +5113,10 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -5361,13 +5301,10 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -5739,13 +5676,10 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -6015,13 +5949,10 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -6818,13 +6749,10 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -7140,13 +7068,10 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -7407,13 +7332,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -7633,13 +7555,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -7860,13 +7779,10 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8032,13 +7948,10 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8204,13 +8117,10 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8399,13 +8309,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8611,13 +8518,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -8798,13 +8702,10 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9009,13 +8910,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9198,13 +9096,10 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9393,13 +9288,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9582,13 +9474,10 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9752,13 +9641,10 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -9940,13 +9826,10 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -10120,13 +10003,10 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -10620,13 +10500,10 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -10875,7 +10752,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": "-6.25000",
|
"scrollX": -6.2500000000000036,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
@@ -10885,13 +10762,10 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": true,
|
"shouldCacheIgnoreZoom": true,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -11005,13 +10879,10 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -11200,13 +11071,10 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -11505,13 +11373,10 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -11913,13 +11778,10 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -12509,13 +12371,10 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -12631,13 +12490,10 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -12954,7 +12810,7 @@ History {
|
|||||||
"id5",
|
"id5",
|
||||||
"id3",
|
"id3",
|
||||||
],
|
],
|
||||||
"index": "a2",
|
"index": "a1V",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
@@ -12969,13 +12825,11 @@ History {
|
|||||||
"id5",
|
"id5",
|
||||||
"id3",
|
"id3",
|
||||||
],
|
],
|
||||||
"index": "a3",
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
"id3",
|
"id3",
|
||||||
],
|
],
|
||||||
"index": "a2",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -13268,13 +13122,10 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
},
|
},
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -13627,13 +13478,10 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
},
|
},
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -13841,7 +13689,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 20,
|
"scrollX": 20,
|
||||||
"scrollY": "-18.53553",
|
"scrollY": -18.535533905932738,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
@@ -13850,13 +13698,10 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": true,
|
"shouldCacheIgnoreZoom": true,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -13970,13 +13815,10 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -14338,13 +14180,10 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
@@ -14459,13 +14298,10 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render } from "./test-utils";
|
import { fireEvent, render } from "./test-utils";
|
||||||
import { Excalidraw, isLinearElement } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
@@ -433,49 +433,4 @@ describe("element binding", () => {
|
|||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not unbind when duplicating via selection group", () => {
|
|
||||||
const rectLeft = UI.createElement("rectangle", {
|
|
||||||
x: 0,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const rectRight = UI.createElement("rectangle", {
|
|
||||||
x: 400,
|
|
||||||
y: 200,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 210,
|
|
||||||
y: 250,
|
|
||||||
width: 177,
|
|
||||||
height: 1,
|
|
||||||
});
|
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
|
||||||
|
|
||||||
mouse.downAt(-100, -100);
|
|
||||||
mouse.moveTo(650, 750);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
expect(API.getSelectedElements().length).toBe(3);
|
|
||||||
|
|
||||||
mouse.moveTo(5, 5);
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
|
||||||
mouse.downAt(5, 5);
|
|
||||||
mouse.moveTo(1000, 1000);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
expect(window.h.elements.length).toBe(6);
|
|
||||||
window.h.elements.forEach((element) => {
|
|
||||||
if (isLinearElement(element)) {
|
|
||||||
expect(element.startBinding).not.toBe(null);
|
|
||||||
expect(element.endBinding).not.toBe(null);
|
|
||||||
} else {
|
|
||||||
expect(element.boundElements).not.toBe(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type { NormalizedZoomValue } from "../types";
|
|||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -436,83 +435,3 @@ describe("pasting & frames", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("clipboard - pasting mermaid definition", () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
mockMermaidToExcalidraw({
|
|
||||||
parseMermaidToExcalidraw: async (definition) => {
|
|
||||||
const lines = definition.split("\n");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (lines.some((line) => line === "flowchart TD")) {
|
|
||||||
resolve({
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
id: "rect1",
|
|
||||||
type: "rectangle",
|
|
||||||
groupIds: [],
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 69.703125,
|
|
||||||
height: 44,
|
|
||||||
strokeWidth: 2,
|
|
||||||
label: {
|
|
||||||
groupIds: [],
|
|
||||||
text: "A",
|
|
||||||
fontSize: 20,
|
|
||||||
},
|
|
||||||
link: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject(new Error("ERROR"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should detect and paste as mermaid", async () => {
|
|
||||||
const text = "flowchart TD\nA";
|
|
||||||
|
|
||||||
pasteWithCtrlCmdV(text);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(h.elements.length).toEqual(2);
|
|
||||||
expect(h.elements).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ type: "rectangle" }),
|
|
||||||
expect.objectContaining({ type: "text", text: "A" }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support directives", async () => {
|
|
||||||
const text = "%%{init: { **config** } }%%\nflowchart TD\nA";
|
|
||||||
|
|
||||||
pasteWithCtrlCmdV(text);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(h.elements.length).toEqual(2);
|
|
||||||
expect(h.elements).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ type: "rectangle" }),
|
|
||||||
expect.objectContaining({ type: "text", text: "A" }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should paste as normal text if invalid mermaid", async () => {
|
|
||||||
const text = "flowchart TD xx\nA";
|
|
||||||
pasteWithCtrlCmdV(text);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(h.elements.length).toEqual(2);
|
|
||||||
expect(h.elements).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ type: "text", text: "flowchart TD xx" }),
|
|
||||||
expect.objectContaining({ type: "text", text: "A" }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ exports[`restoreElements > should restore text element correctly passing value f
|
|||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lineHeight": "1.25000",
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@@ -338,7 +338,7 @@ exports[`restoreElements > should restore text element correctly passing value f
|
|||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": "-8.75000",
|
"y": -8.75,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
|
|||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
"lineHeight": "1.25000",
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
|||||||
@@ -77,6 +77,97 @@ describe("sync invalid indices with array order", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("should NOT sync index when it is already valid", () => {
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A", index: "a2" },
|
||||||
|
{ id: "B", index: "a4" },
|
||||||
|
],
|
||||||
|
movedElements: ["A"],
|
||||||
|
expect: {
|
||||||
|
validInput: true,
|
||||||
|
unchangedElements: ["A", "B"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A", index: "a2" },
|
||||||
|
{ id: "B", index: "a4" },
|
||||||
|
],
|
||||||
|
movedElements: ["B"],
|
||||||
|
expect: {
|
||||||
|
validInput: true,
|
||||||
|
unchangedElements: ["A", "B"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should NOT sync indices when they are already valid", () => {
|
||||||
|
{
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A", index: "a1" },
|
||||||
|
{ id: "B", index: "a0" },
|
||||||
|
{ id: "C", index: "a2" },
|
||||||
|
],
|
||||||
|
movedElements: ["B", "C"],
|
||||||
|
expect: {
|
||||||
|
// this should not sync 'C'
|
||||||
|
unchangedElements: ["A", "C"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A", index: "a1" },
|
||||||
|
{ id: "B", index: "a0" },
|
||||||
|
{ id: "C", index: "a2" },
|
||||||
|
],
|
||||||
|
movedElements: ["A", "B"],
|
||||||
|
expect: {
|
||||||
|
// but this should sync 'A' as it's invalid!
|
||||||
|
unchangedElements: ["C"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A", index: "a0" },
|
||||||
|
{ id: "B", index: "a2" },
|
||||||
|
{ id: "C", index: "a1" },
|
||||||
|
{ id: "D", index: "a1" },
|
||||||
|
{ id: "E", index: "a2" },
|
||||||
|
],
|
||||||
|
movedElements: ["B", "D", "E"],
|
||||||
|
expect: {
|
||||||
|
// should not sync 'E'
|
||||||
|
unchangedElements: ["A", "C", "E"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testMovedIndicesSync({
|
||||||
|
elements: [
|
||||||
|
{ id: "A" },
|
||||||
|
{ id: "B" },
|
||||||
|
{ id: "C", index: "a0" },
|
||||||
|
{ id: "D", index: "a2" },
|
||||||
|
{ id: "E" },
|
||||||
|
{ id: "F", index: "a3" },
|
||||||
|
{ id: "G" },
|
||||||
|
{ id: "H", index: "a1" },
|
||||||
|
{ id: "I", index: "a2" },
|
||||||
|
{ id: "J" },
|
||||||
|
],
|
||||||
|
movedElements: ["A", "B", "D", "E", "F", "G", "J"],
|
||||||
|
expect: {
|
||||||
|
// should not sync 'D' and 'F'
|
||||||
|
unchangedElements: ["C", "D", "F"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("should sync when fractional index is not defined", () => {
|
describe("should sync when fractional index is not defined", () => {
|
||||||
testMovedIndicesSync({
|
testMovedIndicesSync({
|
||||||
elements: [{ id: "A" }],
|
elements: [{ id: "A" }],
|
||||||
@@ -293,122 +384,6 @@ describe("sync invalid indices with array order", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should sync all moved elements regardless of their validity", () => {
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a2" },
|
|
||||||
{ id: "B", index: "a4" },
|
|
||||||
],
|
|
||||||
movedElements: ["A"],
|
|
||||||
expect: {
|
|
||||||
validInput: true,
|
|
||||||
unchangedElements: ["B"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a2" },
|
|
||||||
{ id: "B", index: "a4" },
|
|
||||||
],
|
|
||||||
movedElements: ["B"],
|
|
||||||
expect: {
|
|
||||||
validInput: true,
|
|
||||||
unchangedElements: ["A"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "C", index: "a2" },
|
|
||||||
{ id: "D", index: "a3" },
|
|
||||||
{ id: "A", index: "a0" },
|
|
||||||
{ id: "B", index: "a1" },
|
|
||||||
],
|
|
||||||
movedElements: ["C", "D"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["A", "B"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a1" },
|
|
||||||
{ id: "B", index: "a2" },
|
|
||||||
{ id: "D", index: "a4" },
|
|
||||||
{ id: "C", index: "a3" },
|
|
||||||
{ id: "F", index: "a6" },
|
|
||||||
{ id: "E", index: "a5" },
|
|
||||||
{ id: "H", index: "a8" },
|
|
||||||
{ id: "G", index: "a7" },
|
|
||||||
{ id: "I", index: "a9" },
|
|
||||||
],
|
|
||||||
movedElements: ["D", "F", "H"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["A", "B", "C", "E", "G", "I"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a1" },
|
|
||||||
{ id: "B", index: "a0" },
|
|
||||||
{ id: "C", index: "a2" },
|
|
||||||
],
|
|
||||||
movedElements: ["B", "C"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["A"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a1" },
|
|
||||||
{ id: "B", index: "a0" },
|
|
||||||
{ id: "C", index: "a2" },
|
|
||||||
],
|
|
||||||
movedElements: ["A", "B"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["C"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A", index: "a0" },
|
|
||||||
{ id: "B", index: "a2" },
|
|
||||||
{ id: "C", index: "a1" },
|
|
||||||
{ id: "D", index: "a1" },
|
|
||||||
{ id: "E", index: "a2" },
|
|
||||||
],
|
|
||||||
movedElements: ["B", "D", "E"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["A", "C"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testMovedIndicesSync({
|
|
||||||
elements: [
|
|
||||||
{ id: "A" },
|
|
||||||
{ id: "B" },
|
|
||||||
{ id: "C", index: "a0" },
|
|
||||||
{ id: "D", index: "a2" },
|
|
||||||
{ id: "E" },
|
|
||||||
{ id: "F", index: "a3" },
|
|
||||||
{ id: "G" },
|
|
||||||
{ id: "H", index: "a1" },
|
|
||||||
{ id: "I", index: "a2" },
|
|
||||||
{ id: "J" },
|
|
||||||
],
|
|
||||||
movedElements: ["A", "B", "D", "E", "F", "G", "J"],
|
|
||||||
expect: {
|
|
||||||
unchangedElements: ["C", "H", "I"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("should generate fractions for explicitly moved elements", () => {
|
describe("should generate fractions for explicitly moved elements", () => {
|
||||||
describe("should generate a fraction between 'A' and 'C'", () => {
|
describe("should generate a fraction between 'A' and 'C'", () => {
|
||||||
testMovedIndicesSync({
|
testMovedIndicesSync({
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw";
|
|
||||||
import type { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const mockMermaidToExcalidraw = (opts: {
|
|
||||||
parseMermaidToExcalidraw: typeof parseMermaidToExcalidraw;
|
|
||||||
mockRef?: boolean;
|
|
||||||
}) => {
|
|
||||||
vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => {
|
|
||||||
const module = (await importActual()) as any;
|
|
||||||
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
...module,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const parseMermaidToExcalidrawSpy = vi.spyOn(
|
|
||||||
MermaidToExcalidraw,
|
|
||||||
"parseMermaidToExcalidraw",
|
|
||||||
);
|
|
||||||
|
|
||||||
parseMermaidToExcalidrawSpy.mockImplementation(opts.parseMermaidToExcalidraw);
|
|
||||||
|
|
||||||
if (opts.mockRef) {
|
|
||||||
vi.spyOn(React, "useRef").mockReturnValue({
|
|
||||||
current: {
|
|
||||||
parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -559,10 +559,4 @@ export class UI {
|
|||||||
".context-menu",
|
".context-menu",
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
static queryStats = () => {
|
|
||||||
return GlobalTestState.renderResult.container.querySelector(
|
|
||||||
".Stats",
|
|
||||||
) as HTMLElement | null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import * as textElementUtils from "../element/textElement";
|
|||||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const renderInteractiveScene = vi.spyOn(
|
const renderInteractiveScene = vi.spyOn(
|
||||||
InteractiveCanvas,
|
InteractiveCanvas,
|
||||||
@@ -320,12 +319,12 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
|
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"55.96978",
|
55.9697848965255,
|
||||||
"47.44233",
|
47.442326230998205,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"76.08587",
|
76.08587175006699,
|
||||||
"43.29417",
|
43.294165939653226,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -382,12 +381,12 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"105.96978",
|
105.96978489652551,
|
||||||
"67.44233",
|
67.4423262309982,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"126.08587",
|
126.08587175006699,
|
||||||
"63.29417",
|
63.294165939653226,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -628,16 +627,16 @@ describe("Test Linear Elements", () => {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"85.96978",
|
85.96978489652551,
|
||||||
"77.44233",
|
77.4423262309982,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
70,
|
70,
|
||||||
50,
|
50,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"106.08587",
|
106.08587175006699,
|
||||||
"73.29417",
|
73.29416593965323,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
40,
|
40,
|
||||||
@@ -684,12 +683,12 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"31.88408",
|
31.884084517616053,
|
||||||
"23.13276",
|
23.13275505472383,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"77.74793",
|
77.74792546875662,
|
||||||
"44.57841",
|
44.57840982272327,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -770,12 +769,12 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"55.96978",
|
55.9697848965255,
|
||||||
"47.44233",
|
47.442326230998205,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"76.08587",
|
76.08587175006699,
|
||||||
"43.29417",
|
43.294165939653226,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -929,8 +928,8 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
expect(position).toMatchInlineSnapshot(`
|
expect(position).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"x": "85.82202",
|
"x": 85.82201843191861,
|
||||||
"y": "75.63461",
|
"y": 75.63461309860818,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -973,10 +972,10 @@ describe("Test Linear Elements", () => {
|
|||||||
]);
|
]);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
easy"
|
easy"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
||||||
@@ -1007,10 +1006,10 @@ describe("Test Linear Elements", () => {
|
|||||||
]);
|
]);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
easy"
|
easy"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not bind text to line when double clicked", async () => {
|
it("should not bind text to line when double clicked", async () => {
|
||||||
@@ -1052,11 +1051,11 @@ describe("Test Linear Elements", () => {
|
|||||||
arrayToMap(h.elements),
|
arrayToMap(h.elements),
|
||||||
),
|
),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(textElement.text).toMatchInlineSnapshot(`
|
expect(textElement.text).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
@@ -1069,15 +1068,15 @@ describe("Test Linear Elements", () => {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
20,
|
20,
|
||||||
20,
|
20,
|
||||||
105,
|
105,
|
||||||
80,
|
80,
|
||||||
"55.45894",
|
55.45893770831013,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
UI.resize(container, "ne", [300, 200]);
|
UI.resize(container, "ne", [300, 200]);
|
||||||
|
|
||||||
@@ -1085,7 +1084,7 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"height": 130,
|
"height": 130,
|
||||||
"width": "366.11716",
|
"width": 366.11716195150507,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -1096,11 +1095,11 @@ describe("Test Linear Elements", () => {
|
|||||||
arrayToMap(h.elements),
|
arrayToMap(h.elements),
|
||||||
),
|
),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"x": "271.11716",
|
"x": 271.11716195150507,
|
||||||
"y": 45,
|
"y": 45,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
@@ -1113,15 +1112,15 @@ describe("Test Linear Elements", () => {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
20,
|
20,
|
||||||
35,
|
35,
|
||||||
"501.11716",
|
501.11716195150507,
|
||||||
95,
|
95,
|
||||||
"205.45894",
|
205.4589377083102,
|
||||||
"52.50000",
|
52.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
|
it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
|
||||||
@@ -1350,27 +1349,4 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(label.y).toBe(0);
|
expect(label.y).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test moving linear element points", () => {
|
|
||||||
it("should move the endpoint in the negative direction correctly when the start point is also moved in the positive direction", async () => {
|
|
||||||
const line = createThreePointerLinearElement("arrow");
|
|
||||||
const [origStartX, origStartY] = [line.x, line.y];
|
|
||||||
|
|
||||||
LinearElementEditor.movePoints(line, [
|
|
||||||
{ index: 0, point: [line.points[0][0] + 10, line.points[0][1] + 10] },
|
|
||||||
{
|
|
||||||
index: line.points.length - 1,
|
|
||||||
point: [
|
|
||||||
line.points[line.points.length - 1][0] - 10,
|
|
||||||
line.points[line.points.length - 1][1] - 10,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(line.x).toBe(origStartX + 10);
|
|
||||||
expect(line.y).toBe(origStartY + 10);
|
|
||||||
|
|
||||||
expect(line.points[line.points.length - 1][0]).toBe(20);
|
|
||||||
expect(line.points[line.points.length - 1][1]).toBe(-20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -242,20 +242,3 @@ expect.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializer for IEE754 float pointing numbers to avoid random failures due to tiny precision differences
|
|
||||||
*/
|
|
||||||
expect.addSnapshotSerializer({
|
|
||||||
serialize(val, config, indentation, depth, refs, printer) {
|
|
||||||
return printer(val.toFixed(5), config, indentation, depth, refs);
|
|
||||||
},
|
|
||||||
test(val) {
|
|
||||||
return (
|
|
||||||
typeof val === "number" &&
|
|
||||||
Number.isFinite(val) &&
|
|
||||||
!Number.isNaN(val) &&
|
|
||||||
!Number.isInteger(val)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
// SnapLines
|
// SnapLines
|
||||||
snapLines: AppState["snapLines"];
|
snapLines: AppState["snapLines"];
|
||||||
zenModeEnabled: AppState["zenModeEnabled"];
|
zenModeEnabled: AppState["zenModeEnabled"];
|
||||||
editingElement: AppState["editingElement"];
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -336,11 +335,7 @@ export interface AppState {
|
|||||||
|
|
||||||
fileHandle: FileSystemHandle | null;
|
fileHandle: FileSystemHandle | null;
|
||||||
collaborators: Map<SocketId, Collaborator>;
|
collaborators: Map<SocketId, Collaborator>;
|
||||||
stats: {
|
showStats: boolean;
|
||||||
open: boolean;
|
|
||||||
/** bitmap. Use `STATS_PANELS` bit values */
|
|
||||||
panels: number;
|
|
||||||
};
|
|
||||||
currentChartType: ChartType;
|
currentChartType: ChartType;
|
||||||
pasteDialog:
|
pasteDialog:
|
||||||
| {
|
| {
|
||||||
@@ -444,9 +439,7 @@ export interface ExcalidrawProps {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
) => void;
|
) => void;
|
||||||
initialData?:
|
initialData?: MaybePromise<ExcalidrawInitialDataState | null>;
|
||||||
| (() => MaybePromise<ExcalidrawInitialDataState | null>)
|
|
||||||
| MaybePromise<ExcalidrawInitialDataState | null>;
|
|
||||||
excalidrawAPI?: (api: ExcalidrawImperativeAPI) => void;
|
excalidrawAPI?: (api: ExcalidrawImperativeAPI) => void;
|
||||||
isCollaborating?: boolean;
|
isCollaborating?: boolean;
|
||||||
onPointerUpdate?: (payload: {
|
onPointerUpdate?: (payload: {
|
||||||
@@ -599,7 +592,6 @@ export type AppClassProperties = {
|
|||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
device: App["device"];
|
device: App["device"];
|
||||||
scene: App["scene"];
|
scene: App["scene"];
|
||||||
syncActionResult: App["syncActionResult"];
|
|
||||||
pasteFromClipboard: App["pasteFromClipboard"];
|
pasteFromClipboard: App["pasteFromClipboard"];
|
||||||
id: App["id"];
|
id: App["id"];
|
||||||
onInsertElements: App["onInsertElements"];
|
onInsertElements: App["onInsertElements"];
|
||||||
@@ -614,6 +606,7 @@ export type AppClassProperties = {
|
|||||||
setOpenDialog: App["setOpenDialog"];
|
setOpenDialog: App["setOpenDialog"];
|
||||||
insertEmbeddableElement: App["insertEmbeddableElement"];
|
insertEmbeddableElement: App["insertEmbeddableElement"];
|
||||||
onMagicframeToolSelect: App["onMagicframeToolSelect"];
|
onMagicframeToolSelect: App["onMagicframeToolSelect"];
|
||||||
|
getElementShape: App["getElementShape"];
|
||||||
getName: App["getName"];
|
getName: App["getName"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
-1
@@ -43,7 +43,6 @@ interface ImportMetaEnv {
|
|||||||
VITE_APP_COLLAPSE_OVERLAY: string;
|
VITE_APP_COLLAPSE_OVERLAY: string;
|
||||||
// Enable eslint in dev server
|
// Enable eslint in dev server
|
||||||
VITE_APP_ENABLE_ESLINT: string;
|
VITE_APP_ENABLE_ESLINT: string;
|
||||||
VITE_APP_ENABLE_TRACKING: string;
|
|
||||||
|
|
||||||
VITE_PKG_NAME: string;
|
VITE_PKG_NAME: string;
|
||||||
VITE_PKG_VERSION: string;
|
VITE_PKG_VERSION: string;
|
||||||
|
|||||||
@@ -84,13 +84,10 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
|
"showStats": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"stats": {
|
|
||||||
"open": false,
|
|
||||||
"panels": 3,
|
|
||||||
},
|
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
@@ -1930,10 +1930,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
|
resolved "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
|
||||||
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
|
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
|
||||||
|
|
||||||
"@excalidraw/mermaid-to-excalidraw@1.1.0":
|
"@excalidraw/mermaid-to-excalidraw@1.0.0":
|
||||||
version "1.1.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.0.tgz#a24a7aa3ad2e4f671054fdb670a8508bab463814"
|
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.0.0.tgz#8c058d2a43230425cba96d01e4a669a2d7c586a2"
|
||||||
integrity sha512-YP2roqrImzek1SpUAeToSTNhH5Gfw9ogdI5KHp7c+I/mX7SEW8oNqqX7CP+oHcUgNF6RrYIkqSrnMRN9/3EGLg==
|
integrity sha512-RGSoJBY2gFag6mQOIwa3OakTrvAZYx0bwvnr5ojuCZInih8Fxhje4X1WZfsaQx+GATEH8Ioq3O3b1FPDg4nKjQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@excalidraw/markdown-to-text" "0.1.2"
|
"@excalidraw/markdown-to-text" "0.1.2"
|
||||||
mermaid "10.9.0"
|
mermaid "10.9.0"
|
||||||
@@ -7753,9 +7753,9 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
lz-string@1.5.0, lz-string@^1.5.0:
|
lz-string@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||||
|
|
||||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||||
|
|||||||
Reference in New Issue
Block a user