Compare commits

..

5 Commits

Author SHA1 Message Date
dwelle 76fdb0ae59 Merge branch 'master' into fix-frame
# Conflicts:
#	packages/excalidraw/scene/selection.ts
2024-01-19 13:51:39 +01:00
dwelle 9919d7d7e2 Merge branch 'master' into fix-frame 2023-08-18 16:34:45 +02:00
dwelle a2978a4783 Merge branch 'master' into fix-frame
# Conflicts:
#	src/actions/actionAlign.tsx
#	src/actions/actionDistribute.tsx
#	src/actions/actionFlip.ts
#	src/components/App.tsx
#	src/scene/selection.ts
2023-08-18 16:31:09 +02:00
Ryan Di 5a9f3dfdd8 fix improper duplication for texts inside frame 2023-06-23 21:33:32 +08:00
Ryan Di dce6010b29 fix new element scene null leading to bugs after aligning 2023-06-23 21:31:28 +08:00
110 changed files with 1139 additions and 3089 deletions
+1 -1
View File
@@ -23,5 +23,5 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release
run: |
yarn add @actions/core -W
yarn add @actions/core
yarn autorelease
-1
View File
@@ -25,4 +25,3 @@ packages/excalidraw/types
coverage
dev-dist
html
examples/**/bundle.*
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
| [onChange](#onChange) | `function` | Subscribes to change events |
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
@@ -32,9 +32,15 @@ function App() {
### Next.js
Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
If you want to only import `Excalidraw` component you can do :point_down:
Here are two ways on how you can render **Excalidraw** on **Next.js**.
1. Using **Next.js Dynamic** import [Recommended].
Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
```jsx showLineNumbers
import dynamic from "next/dynamic";
@@ -49,88 +55,25 @@ export default function App() {
}
```
However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
<Tabs>
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
2. Importing Excalidraw once **client** is rendered.
```jsx showLineNumbers
"use client";
import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
const ExcalidrawWrapper: React.FC = () => {
console.info(convertToExcalidrawElements([{
type: "rectangle",
id: "rect-1",
width: 186.47265625,
height: 141.9765625,
},]));
return (
<div style={{height:"500px", width:"500px"}}
<Excalidraw />
</div>
```jsx showLineNumbers
import { useState, useEffect } from "react";
export default function App() {
const [Excalidraw, setExcalidraw] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) =>
setExcalidraw(comp.Excalidraw),
);
};
export default ExcalidrawWrapper;
```
</TabItem>
<TabItem value="pages" label="Pages router">
```jsx showLineNumbers
import dynamic from "next/dynamic";
// Since client components get prerenderd on server as well hence importing
// the excalidraw stuff dynamically with ssr false
const ExcalidrawWrapper = dynamic(
async () => (await import("../excalidrawWrapper")).default,
{
ssr: false,
},
);
export default function Page() {
return (
<ExcalidrawWrapper />
);
}
```
</TabItem>
<TabItem value="app" label="App router">
```jsx showLineNumbers
import dynamic from "next/dynamic";
// Since client components get prerenderd on server as well hence importing
// the excalidraw stuff dynamically with ssr false
const ExcalidrawWrapper = dynamic(
async () => (await import("../excalidrawWrapper")).default,
{
ssr: false,
},
);
export default function Page() {
return (
<ExcalidrawWrapper />
);
}
```
</TabItem>
</Tabs>
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
}, []);
return <>{Excalidraw && <Excalidraw />}</>;
}
```
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
@@ -1,27 +0,0 @@
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw";
const MobileFooter = ({
excalidrawAPI,
excalidrawLib,
}: {
excalidrawAPI: ExcalidrawImperativeAPI;
excalidrawLib: typeof TExcalidraw;
}) => {
const { useDevice, Footer } = excalidrawLib;
const device = useDevice();
if (device.editor.isMobile) {
return (
<Footer>
<CustomFooter
excalidrawAPI={excalidrawAPI}
excalidrawLib={excalidrawLib}
/>
</Footer>
);
}
return null;
};
export default MobileFooter;
-13
View File
@@ -1,13 +0,0 @@
{
"name": "examples",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"@excalidraw/excalidraw": "*"
},
"devDependencies": {
"typescript": "^5"
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig"
}
-146
View File
@@ -1,146 +0,0 @@
import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access";
import type { MIME_TYPES } from "@excalidraw/excalidraw";
import { AbortError } from "../../packages/excalidraw/errors";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500;
export type ResolvablePromise<T> = Promise<T> & {
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
reject: (error: Error) => void;
};
export const resolvablePromise = <T>() => {
let resolve!: any;
let reject!: any;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
(promise as any).resolve = resolve;
(promise as any).reject = reject;
return promise as ResolvablePromise<T>;
};
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
const xd = x2 - x1;
const yd = y2 - y1;
return Math.hypot(xd, yd);
};
export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description: string;
multiple?: M;
}): Promise<M extends false | undefined ? File : File[]> => {
// an unsafe TS hack, alas not much we can do AFAIK
type RetType = M extends false | undefined ? File : File[];
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
mimeTypes.push(MIME_TYPES[type]);
return mimeTypes;
}, [] as string[]);
const extensions = opts.extensions?.reduce((acc, ext) => {
if (ext === "jpg") {
return acc.concat(".jpg", ".jpeg");
}
return acc.concat(`.${ext}`);
}, [] as string[]);
return _fileOpen({
description: opts.description,
extensions,
mimeTypes,
multiple: opts.multiple ?? false,
legacySetup: (resolve, reject, input) => {
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
const focusHandler = () => {
checkForFile();
document.addEventListener("keyup", scheduleRejection);
document.addEventListener("pointerup", scheduleRejection);
scheduleRejection();
};
const checkForFile = () => {
// this hack might not work when expecting multiple files
if (input.files?.length) {
const ret = opts.multiple ? [...input.files] : input.files[0];
resolve(ret as RetType);
}
};
requestAnimationFrame(() => {
window.addEventListener("focus", focusHandler);
});
const interval = window.setInterval(() => {
checkForFile();
}, INPUT_CHANGE_INTERVAL_MS);
return (rejectPromise) => {
clearInterval(interval);
scheduleRejection.cancel();
window.removeEventListener("focus", focusHandler);
document.removeEventListener("keyup", scheduleRejection);
document.removeEventListener("pointerup", scheduleRejection);
if (rejectPromise) {
// so that something is shown in console if we need to debug this
console.warn("Opening the file was canceled (legacy-fs).");
rejectPromise(new AbortError());
}
};
},
}) as Promise<RetType>;
};
export const debounce = <T extends any[]>(
fn: (...args: T) => void,
timeout: number,
) => {
let handle = 0;
let lastArgs: T | null = null;
const ret = (...args: T) => {
lastArgs = args;
clearTimeout(handle);
handle = window.setTimeout(() => {
lastArgs = null;
fn(...args);
}, timeout);
};
ret.flush = () => {
clearTimeout(handle);
if (lastArgs) {
const _lastArgs = lastArgs;
lastArgs = null;
fn(..._lastArgs);
}
};
ret.cancel = () => {
lastArgs = null;
clearTimeout(handle);
};
return ret;
};
export const withBatchedUpdates = <
TFunction extends ((event: any) => void) | (() => void),
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) =>
((event) => {
unstable_batchedUpdates(func as TFunction, event);
}) as TFunction;
/**
* barches React state updates and throttles the calls to a single call per
* animation frame
*/
export const withBatchedUpdatesThrottled = <
TFunction extends ((event: any) => void) | (() => void),
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) => {
// @ts-ignore
return throttleRAF<Parameters<TFunction>>(((event) => {
unstable_batchedUpdates(func, event);
}) as TFunction);
};
@@ -1,36 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
@@ -1,12 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
distDir: "build",
typescript: {
// The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
ignoreBuildErrors: true,
},
// This is needed as in pages router the code for importing types throws error as its outside next js app
transpilePackages: ["../"],
};
module.exports = nextConfig;
@@ -1,25 +0,0 @@
{
"name": "with-nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
"dev": "yarn build:workspace && next dev -p 3005",
"build": "yarn build:workspace && next build",
"start": "next start -p 3006",
"lint": "next lint"
},
"dependencies": {
"@excalidraw/excalidraw": "*",
"next": "14.1",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"path2d-polyfill": "2.0.1",
"typescript": "^5"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -1,11 +0,0 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
@@ -1,23 +0,0 @@
import dynamic from "next/dynamic";
import "../common.scss";
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
// with ssr false
const ExcalidrawWithClientOnly = dynamic(
async () => (await import("../excalidrawWrapper")).default,
{
ssr: false,
},
);
export default function Page() {
return (
<>
<a href="/excalidraw-in-pages">Switch to Pages router</a>
<h1 className="page-title">App Router</h1>
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
<ExcalidrawWithClientOnly />
</>
);
}
@@ -1,15 +0,0 @@
* {
box-sizing: border-box;
font-family: sans-serif;
}
a {
color: #1c7ed6;
font-size: 20px;
text-decoration: none;
font-weight: 550;
}
.page-title {
text-align: center;
}
@@ -1,22 +0,0 @@
"use client";
import * as excalidrawLib from "@excalidraw/excalidraw";
import { Excalidraw } from "@excalidraw/excalidraw";
import App from "../../components/App";
import "@excalidraw/excalidraw/index.css";
const ExcalidrawWrapper: React.FC = () => {
return (
<>
<App
appTitle={"Excalidraw with Nextjs Example"}
useCustom={(api: any, args?: any[]) => {}}
excalidrawLib={excalidrawLib}
>
<Excalidraw />
</App>
</>
);
};
export default ExcalidrawWrapper;
@@ -1,22 +0,0 @@
import dynamic from "next/dynamic";
import "../common.scss";
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
// with ssr false
const Excalidraw = dynamic(
async () => (await import("../excalidrawWrapper")).default,
{
ssr: false,
},
);
export default function Page() {
return (
<>
<a href="/">Switch to App router</a>
<h1 className="page-title">Pages Router</h1>
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
<Excalidraw />
</>
);
}
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
},
"forceConsistentCasingInFileNames": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
"exclude": ["node_modules"]
}
@@ -1,3 +0,0 @@
{
"outputDirectory": "build"
}
-252
View File
@@ -1,252 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@excalidraw/excalidraw@workspace:^":
version "0.17.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
"@next/env@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
"@next/swc-darwin-arm64@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
"@next/swc-darwin-x64@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
"@next/swc-linux-arm64-gnu@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
"@next/swc-linux-arm64-musl@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
"@next/swc-linux-x64-gnu@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
"@next/swc-linux-x64-musl@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
"@next/swc-win32-arm64-msvc@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
"@next/swc-win32-ia32-msvc@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
"@next/swc-win32-x64-msvc@14.0.4":
version "14.0.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
"@swc/helpers@0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
dependencies:
tslib "^2.4.0"
"@types/node@^20":
version "20.11.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
dependencies:
undici-types "~5.26.4"
"@types/prop-types@*":
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
"@types/react-dom@^18":
version "18.2.18"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18":
version "18.2.47"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.8"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
busboy@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
caniuse-lite@^1.0.30001406:
version "1.0.30001576"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
csstype@^3.0.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
glob-to-regexp@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
graceful-fs@^4.1.2, graceful-fs@^4.2.11:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
nanoid@^3.3.6:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
next@14.0.4:
version "14.0.4"
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
dependencies:
"@next/env" "14.0.4"
"@swc/helpers" "0.5.2"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
graceful-fs "^4.2.11"
postcss "8.4.31"
styled-jsx "5.1.1"
watchpack "2.4.0"
optionalDependencies:
"@next/swc-darwin-arm64" "14.0.4"
"@next/swc-darwin-x64" "14.0.4"
"@next/swc-linux-arm64-gnu" "14.0.4"
"@next/swc-linux-arm64-musl" "14.0.4"
"@next/swc-linux-x64-gnu" "14.0.4"
"@next/swc-linux-x64-musl" "14.0.4"
"@next/swc-win32-arm64-msvc" "14.0.4"
"@next/swc-win32-ia32-msvc" "14.0.4"
"@next/swc-win32-x64-msvc" "14.0.4"
path2d-polyfill@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
react-dom@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
styled-jsx@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
dependencies:
client-only "0.0.1"
tslib@^2.4.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
typescript@^5:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
watchpack@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
@@ -1,28 +0,0 @@
import App from "../components/App";
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import type * as TExcalidraw from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
declare global {
interface Window {
ExcalidrawLib: typeof TExcalidraw;
}
}
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
const { Excalidraw } = window.ExcalidrawLib;
root.render(
<StrictMode>
<App
appTitle={"Excalidraw Example"}
useCustom={(api: any, args?: any[]) => {}}
excalidrawLib={window.ExcalidrawLib}
>
<Excalidraw />
</App>
</StrictMode>,
);
@@ -1,19 +0,0 @@
{
"name": "with-script-in-browser",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0",
"@excalidraw/excalidraw": "*"
},
"devDependencies": {
"vite": "5.0.12",
"typescript": "^5"
},
"scripts": {
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
"build:preview": "yarn build && vite preview --port 5002"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

@@ -1,11 +0,0 @@
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 3001,
// open the browser
open: true,
},
publicDir: "public",
});
-313
View File
@@ -1,313 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/aix-ppc64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
"@esbuild/android-arm64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
"@esbuild/android-arm@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
"@esbuild/android-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
"@esbuild/darwin-arm64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
"@esbuild/darwin-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
"@esbuild/freebsd-arm64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
"@esbuild/freebsd-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
"@esbuild/linux-arm64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
"@esbuild/linux-arm@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
"@esbuild/linux-ia32@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
"@esbuild/linux-loong64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
"@esbuild/linux-mips64el@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
"@esbuild/linux-ppc64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
"@esbuild/linux-riscv64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
"@esbuild/linux-s390x@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
"@esbuild/linux-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
"@esbuild/netbsd-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
"@esbuild/openbsd-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
"@esbuild/sunos-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
"@esbuild/win32-arm64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
"@esbuild/win32-ia32@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
"@esbuild/win32-x64@0.19.11":
version "0.19.11"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
"@rollup/rollup-android-arm-eabi@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
"@rollup/rollup-android-arm64@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
"@rollup/rollup-darwin-arm64@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
"@rollup/rollup-darwin-x64@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
"@rollup/rollup-linux-arm-gnueabihf@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
"@rollup/rollup-linux-arm64-gnu@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
"@rollup/rollup-linux-arm64-musl@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
"@rollup/rollup-linux-riscv64-gnu@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
"@rollup/rollup-linux-x64-gnu@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
"@rollup/rollup-linux-x64-musl@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
"@rollup/rollup-win32-arm64-msvc@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
"@rollup/rollup-win32-ia32-msvc@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
"@rollup/rollup-win32-x64-msvc@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
"@types/estree@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
esbuild@^0.19.3:
version "0.19.11"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
optionalDependencies:
"@esbuild/aix-ppc64" "0.19.11"
"@esbuild/android-arm" "0.19.11"
"@esbuild/android-arm64" "0.19.11"
"@esbuild/android-x64" "0.19.11"
"@esbuild/darwin-arm64" "0.19.11"
"@esbuild/darwin-x64" "0.19.11"
"@esbuild/freebsd-arm64" "0.19.11"
"@esbuild/freebsd-x64" "0.19.11"
"@esbuild/linux-arm" "0.19.11"
"@esbuild/linux-arm64" "0.19.11"
"@esbuild/linux-ia32" "0.19.11"
"@esbuild/linux-loong64" "0.19.11"
"@esbuild/linux-mips64el" "0.19.11"
"@esbuild/linux-ppc64" "0.19.11"
"@esbuild/linux-riscv64" "0.19.11"
"@esbuild/linux-s390x" "0.19.11"
"@esbuild/linux-x64" "0.19.11"
"@esbuild/netbsd-x64" "0.19.11"
"@esbuild/openbsd-x64" "0.19.11"
"@esbuild/sunos-x64" "0.19.11"
"@esbuild/win32-arm64" "0.19.11"
"@esbuild/win32-ia32" "0.19.11"
"@esbuild/win32-x64" "0.19.11"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
postcss@^8.4.32:
version "8.4.33"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
rollup@^4.2.0:
version "4.9.5"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.9.5"
"@rollup/rollup-android-arm64" "4.9.5"
"@rollup/rollup-darwin-arm64" "4.9.5"
"@rollup/rollup-darwin-x64" "4.9.5"
"@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
"@rollup/rollup-linux-arm64-gnu" "4.9.5"
"@rollup/rollup-linux-arm64-musl" "4.9.5"
"@rollup/rollup-linux-riscv64-gnu" "4.9.5"
"@rollup/rollup-linux-x64-gnu" "4.9.5"
"@rollup/rollup-linux-x64-musl" "4.9.5"
"@rollup/rollup-win32-arm64-msvc" "4.9.5"
"@rollup/rollup-win32-ia32-msvc" "4.9.5"
"@rollup/rollup-win32-x64-msvc" "4.9.5"
fsevents "~2.3.2"
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
vite@5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.32"
rollup "^4.2.0"
optionalDependencies:
fsevents "~2.3.3"
+2 -4
View File
@@ -4,9 +4,7 @@
"workspaces": [
"excalidraw-app",
"packages/excalidraw",
"packages/utils",
"examples/excalidraw",
"examples/excalidraw/*"
"packages/utils"
],
"dependencies": {
"@excalidraw/random-username": "1.0.0",
@@ -45,7 +43,7 @@
"prettier": "2.6.2",
"rewire": "6.0.0",
"typescript": "4.9.4",
"vite": "5.0.12",
"vite": "5.0.6",
"vite-plugin-checker": "0.6.1",
"vite-plugin-ejs": "1.7.0",
"vite-plugin-pwa": "0.17.4",
+2
View File
@@ -1,2 +1,4 @@
node_modules
types
bundle.js
bundle.css
+3 -11
View File
@@ -11,7 +11,6 @@ import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
@@ -40,20 +39,13 @@ const alignSelectedElements = (
alignment: Alignment,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = arrayToMap(elements);
const updatedElements = alignElements(
selectedElements,
elementsMap,
alignment,
);
const updatedElements = alignElements(selectedElements, alignment);
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
@@ -45,9 +45,8 @@ export const actionUnbindText = register({
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = app.scene.getNonDeletedElementsMap();
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height, baseline } = measureText(
boundTextElement.originalText,
@@ -107,10 +106,7 @@ export const actionBindText = register({
if (
textElement &&
bindingContainer &&
getBoundTextElement(
bindingContainer,
app.scene.getNonDeletedElementsMap(),
) === null
getBoundTextElement(bindingContainer) === null
) {
return true;
}
@@ -1,84 +0,0 @@
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import {
FontSizeExtraLargeIcon,
FontSizeLargeIcon,
FontSizeMediumIcon,
FontSizeSmallIcon,
} from "../components/icons";
import { DEFAULT_FONT_SIZE } from "../constants";
import { isTextElement } from "../element";
import { getBoundTextElement } from "../element/textElement";
import { t } from "../i18n";
import { changeFontSize } from "./utils";
import { getFormValue } from "./actionProperties";
import { register } from "./register";
export const actionChangeFontSize = register({
name: "changeFontSize",
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, () => value, value);
},
PanelComponent: ({ elements, appState, updateData, app }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonIconSelect
group="font-size"
options={[
{
value: 16,
text: t("labels.small"),
icon: FontSizeSmallIcon,
testId: "fontSize-small",
},
{
value: 20,
text: t("labels.medium"),
icon: FontSizeMediumIcon,
testId: "fontSize-medium",
},
{
value: 28,
text: t("labels.large"),
icon: FontSizeLargeIcon,
testId: "fontSize-large",
},
{
value: 36,
text: t("labels.veryLarge"),
icon: FontSizeExtraLargeIcon,
testId: "fontSize-veryLarge",
},
]}
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
(element) =>
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
(hasSelection) =>
hasSelection
? null
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>
),
});
@@ -1,26 +0,0 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { changeFontSize, FONT_SIZE_RELATIVE_INCREASE_STEP } from "./utils";
export const actionDecreaseFontSize = register({
name: "decreaseFontSize",
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, (element) =>
Math.round(
// get previous value before relative increase (doesn't work fully
// due to rounding and float precision issues)
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.COMMA needed for MacOS
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
);
},
});
@@ -7,7 +7,6 @@ import { distributeElements, Distribution } from "../distribute";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
@@ -32,18 +31,12 @@ const distributeSelectedElements = (
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const updatedElements = distributeElements(
selectedElements,
app.scene.getNonDeletedElementsMap(),
distribution,
);
const updatedElements = distributeElements(selectedElements, distribution);
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
@@ -104,8 +104,8 @@ const duplicateElements = (
const idsOfElementsToDuplicate = arrayToMap(
getSelectedElements(sortedElements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
includeBoundTextElement: false,
includeElementsInFrames: false,
}),
);
@@ -139,7 +139,7 @@ const duplicateElements = (
continue;
}
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
const boundTextElement = getBoundTextElement(element);
const isElementAFrameLike = isFrameLikeElement(element);
if (idsOfElementsToDuplicate.get(element.id)) {
+10 -33
View File
@@ -1,14 +1,9 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import {
ExcalidrawElement,
NonDeleted,
NonDeletedElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements";
import { AppState } from "../types";
import { AppState, PointerDownState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
@@ -24,16 +19,7 @@ export const actionFlipHorizontal = register({
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(
elements,
app.scene.getNonDeletedElementsMap(),
appState,
"horizontal",
),
appState,
app,
),
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
@@ -48,12 +34,7 @@ export const actionFlipVertical = register({
perform: (elements, appState, _, app) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(
elements,
app.scene.getNonDeletedElementsMap(),
appState,
"vertical",
),
flipSelectedElements(elements, appState, "vertical"),
appState,
app,
),
@@ -68,7 +49,6 @@ export const actionFlipVertical = register({
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
@@ -83,7 +63,6 @@ const flipSelectedElements = (
const updatedElements = flipElements(
selectedElements,
elementsMap,
appState,
flipDirection,
);
@@ -96,17 +75,15 @@ const flipSelectedElements = (
};
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
resizeMultipleElements(
elementsMap,
selectedElements,
elementsMap,
{ originalElements: arrayToMap(elements) } as PointerDownState,
elements,
"nw",
true,
flipDirection === "horizontal" ? maxX : minX,
@@ -115,7 +92,7 @@ const flipElements = (
(isBindingEnabled(appState)
? bindOrUnbindSelectedElements
: unbindLinearElements)(selectedElements);
: unbindLinearElements)(elements);
return selectedElements;
return elements;
};
+5 -1
View File
@@ -63,7 +63,11 @@ export const actionRemoveAllElementsFromFrame = register({
if (isFrameLikeElement(selectedElement)) {
return {
elements: removeAllElementsFromFrame(elements, selectedElement),
elements: removeAllElementsFromFrame(
elements,
selectedElement,
appState,
),
appState: {
...appState,
selectedElementIds: {
+4 -3
View File
@@ -105,9 +105,10 @@ export const actionGroup = register({
const frameElementsMap = groupByFrameLikes(selectedElements);
frameElementsMap.forEach((elementsInFrame, frameId) => {
removeElementsFromFrame(
nextElements = removeElementsFromFrame(
nextElements,
elementsInFrame,
app.scene.getNonDeletedElementsMap(),
appState,
);
});
}
@@ -228,7 +229,7 @@ export const actionUngroup = register({
nextElements,
getElementsInResizingFrame(nextElements, frame, appState),
frame,
app,
appState,
);
}
});
@@ -1,21 +0,0 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { changeFontSize, FONT_SIZE_RELATIVE_INCREASE_STEP } from "./utils";
export const actionIncreaseFontSize = register({
name: "increaseFontSize",
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, (element) =>
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.PERIOD needed for MacOS
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
);
},
});
@@ -57,9 +57,7 @@ export const actionGoToCollaborator = register({
isBeingFollowed={isBeingFollowed}
isCurrentUser={collaborator.isCurrentUser === true}
/>
<div className="UserList__collaborator-name">
{collaborator.username}
</div>
{collaborator.username}
<div
className="UserList__collaborator-follow-status-icon"
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
+201 -45
View File
@@ -32,6 +32,10 @@ import {
StrokeWidthBaseIcon,
StrokeWidthBoldIcon,
StrokeWidthExtraBoldIcon,
FontSizeSmallIcon,
FontSizeMediumIcon,
FontSizeLargeIcon,
FontSizeExtraLargeIcon,
EdgeSharpIcon,
EdgeRoundIcon,
FreedrawIcon,
@@ -48,6 +52,7 @@ import {
} from "../components/icons";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
ROUNDNESS,
STROKE_WIDTH,
@@ -58,12 +63,17 @@ import {
isTextElement,
redrawTextBoundingBox,
} from "../element";
import { newElementWith } from "../element/mutateElement";
import { mutateElement, newElementWith } from "../element/mutateElement";
import {
getBoundTextElement,
getContainerElement,
getDefaultLineHeight,
} from "../element/textElement";
import { isLinearElement, isUsingAdaptiveRadius } from "../element/typeChecks";
import {
isBoundToContainer,
isLinearElement,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
Arrowhead,
ExcalidrawElement,
@@ -74,6 +84,7 @@ import {
VerticalAlign,
} from "../element/types";
import { getLanguage, t } from "../i18n";
import { KEYS } from "../keys";
import { randomInteger } from "../random";
import {
canHaveArrowheads,
@@ -86,6 +97,8 @@ import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
export const changeProperty = (
elements: readonly ExcalidrawElement[],
appState: AppState,
@@ -149,6 +162,74 @@ export const getFormValue = function <T extends Primitive>(
return ret;
};
const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement)) {
return nextElement;
}
return mutateElement(
nextElement,
{
x:
prevElement.textAlign === "left"
? prevElement.x
: prevElement.x +
(prevElement.width - nextElement.width) /
(prevElement.textAlign === "center" ? 2 : 1),
// centering vertically is non-standard, but for Excalidraw I think
// it makes sense
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
},
false,
);
};
const changeFontSize = (
elements: readonly ExcalidrawElement[],
appState: AppState,
getNewFontSize: (element: ExcalidrawTextElement) => number,
fallbackValue?: ExcalidrawTextElement["fontSize"],
) => {
const newFontSizes = new Set<number>();
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newFontSize = getNewFontSize(oldElement);
newFontSizes.add(newFontSize);
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
fontSize: newFontSize,
});
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
newElement = offsetElementAfterFontResize(oldElement, newElement);
return newElement;
}
return oldElement;
},
true,
),
appState: {
...appState,
// update state only if we've set all select text elements to
// the same font size
currentItemFontSize:
newFontSizes.size === 1
? [...newFontSizes][0]
: fallbackValue ?? appState.currentItemFontSize,
},
commitToHistory: true,
};
};
// -----------------------------------------------------------------------------
export const actionChangeStrokeColor = register({
@@ -516,10 +597,113 @@ export const actionChangeOpacity = register({
),
});
export const actionChangeFontSize = register({
name: "changeFontSize",
trackEvent: false,
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, () => value, value);
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonIconSelect
group="font-size"
options={[
{
value: 16,
text: t("labels.small"),
icon: FontSizeSmallIcon,
testId: "fontSize-small",
},
{
value: 20,
text: t("labels.medium"),
icon: FontSizeMediumIcon,
testId: "fontSize-medium",
},
{
value: 28,
text: t("labels.large"),
icon: FontSizeLargeIcon,
testId: "fontSize-large",
},
{
value: 36,
text: t("labels.veryLarge"),
icon: FontSizeExtraLargeIcon,
testId: "fontSize-veryLarge",
},
]}
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
(element) =>
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection
? null
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>
),
});
export const actionDecreaseFontSize = register({
name: "decreaseFontSize",
trackEvent: false,
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, (element) =>
Math.round(
// get previous value before relative increase (doesn't work fully
// due to rounding and float precision issues)
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.COMMA needed for MacOS
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
);
},
});
export const actionIncreaseFontSize = register({
name: "increaseFontSize",
trackEvent: false,
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, (element) =>
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.PERIOD needed for MacOS
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
);
},
});
export const actionChangeFontFamily = register({
name: "changeFontFamily",
trackEvent: false,
perform: (elements, appState, value, app) => {
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
@@ -533,10 +717,7 @@ export const actionChangeFontFamily = register({
lineHeight: getDefaultLineHeight(value),
},
);
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
);
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
return newElement;
}
@@ -551,7 +732,7 @@ export const actionChangeFontFamily = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
PanelComponent: ({ elements, appState, updateData }) => {
const options: {
value: FontFamilyValues;
text: string;
@@ -591,21 +772,14 @@ export const actionChangeFontFamily = register({
if (isTextElement(element)) {
return element.fontFamily;
}
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontFamily;
}
return null;
},
(element) =>
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection
? null
@@ -621,7 +795,7 @@ export const actionChangeFontFamily = register({
export const actionChangeTextAlign = register({
name: "changeTextAlign",
trackEvent: false,
perform: (elements, appState, value, app) => {
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
@@ -632,10 +806,7 @@ export const actionChangeTextAlign = register({
oldElement,
{ textAlign: value },
);
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
);
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
return newElement;
}
@@ -650,8 +821,7 @@ export const actionChangeTextAlign = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
PanelComponent: ({ elements, appState, updateData }) => {
return (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
@@ -684,18 +854,14 @@ export const actionChangeTextAlign = register({
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(
element,
elementsMap,
);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
(element) =>
isTextElement(element) ||
getBoundTextElement(element, elementsMap) !== null,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection ? null : appState.currentItemTextAlign,
)}
@@ -709,7 +875,7 @@ export const actionChangeTextAlign = register({
export const actionChangeVerticalAlign = register({
name: "changeVerticalAlign",
trackEvent: { category: "element" },
perform: (elements, appState, value, app) => {
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
@@ -721,10 +887,7 @@ export const actionChangeVerticalAlign = register({
{ verticalAlign: value },
);
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
);
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
return newElement;
}
@@ -738,7 +901,7 @@ export const actionChangeVerticalAlign = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
PanelComponent: ({ elements, appState, updateData }) => {
return (
<fieldset>
<ButtonIconSelect<VerticalAlign | false>
@@ -770,21 +933,14 @@ export const actionChangeVerticalAlign = register({
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.verticalAlign;
}
return null;
},
(element) =>
isTextElement(element) ||
getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
) !== null,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
)}
onChange={(value) => updateData(value)}
+3 -6
View File
@@ -32,15 +32,12 @@ export let copiedStyles: string = "{}";
export const actionCopyStyles = register({
name: "copyStyles",
trackEvent: { category: "element" },
perform: (elements, appState, formData, app) => {
perform: (elements, appState) => {
const elementsCopied = [];
const element = elements.find((el) => appState.selectedElementIds[el.id]);
elementsCopied.push(element);
if (element && hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap(),
);
const boundTextElement = getBoundTextElement(element);
elementsCopied.push(boundTextElement);
}
if (element) {
@@ -62,7 +59,7 @@ export const actionCopyStyles = register({
export const actionPasteStyles = register({
name: "pasteStyles",
trackEvent: { category: "element" },
perform: (elements, appState, formData, app) => {
perform: (elements, appState) => {
const elementsCopied = JSON.parse(copiedStyles);
const pastedElement = elementsCopied[0];
const boundTextElement = elementsCopied[1];
+1 -6
View File
@@ -14,17 +14,12 @@ export {
actionChangeFillStyle,
actionChangeSloppiness,
actionChangeOpacity,
actionChangeFontSize,
actionChangeFontFamily,
actionChangeTextAlign,
actionChangeVerticalAlign,
} from "./actionProperties";
export { actionDecreaseFontSize } from "./actionDecreaseFontSize";
export { actionIncreaseFontSize } from "./actionIncreaseFontSize";
export { actionChangeFontSize } from "./actionChangeFontSize";
export {
actionChangeViewBackgroundColor,
actionClearCanvas,
-80
View File
@@ -1,80 +0,0 @@
import { mutateElement, newElementWith } from "..";
import { isTextElement, redrawTextBoundingBox } from "../element";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { changeProperty } from "./actionProperties";
export const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement)) {
return nextElement;
}
return mutateElement(
nextElement,
{
x:
prevElement.textAlign === "left"
? prevElement.x
: prevElement.x +
(prevElement.width - nextElement.width) /
(prevElement.textAlign === "center" ? 2 : 1),
// centering vertically is non-standard, but for Excalidraw I think
// it makes sense
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
},
false,
);
};
export const changeFontSize = (
elements: readonly ExcalidrawElement[],
appState: AppState,
app: AppClassProperties,
getNewFontSize: (element: ExcalidrawTextElement) => number,
fallbackValue?: ExcalidrawTextElement["fontSize"],
) => {
const newFontSizes = new Set<number>();
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newFontSize = getNewFontSize(oldElement);
newFontSizes.add(newFontSize);
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
fontSize: newFontSize,
});
redrawTextBoundingBox(
newElement,
app.scene.getContainerElement(oldElement),
);
newElement = offsetElementAfterFontResize(oldElement, newElement);
return newElement;
}
return oldElement;
},
true,
),
appState: {
...appState,
// update state only if we've set all select text elements to
// the same font size
currentItemFontSize:
newFontSizes.size === 1
? [...newFontSizes][0]
: fallbackValue ?? appState.currentItemFontSize,
},
commitToHistory: true,
};
};
+3 -6
View File
@@ -1,4 +1,4 @@
import { ElementsMap, ExcalidrawElement } from "./element/types";
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
@@ -10,13 +10,10 @@ export interface Alignment {
export const alignElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment,
): ExcalidrawElement[] => {
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements,
elementsMap,
);
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
return groups.flatMap((group) => {
+11 -11
View File
@@ -1,10 +1,7 @@
import { useState } from "react";
import React, { useState } from "react";
import { ActionManager } from "../actions/manager";
import {
ExcalidrawElementType,
NonDeletedElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
import { t } from "../i18n";
import { useDevice } from "./App";
import {
@@ -47,14 +44,17 @@ import { useTunnels } from "../context/tunnels";
export const SelectedShapeActions = ({
appState,
elementsMap,
elements,
renderAction,
}: {
appState: UIAppState;
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
}) => {
const targetElements = getTargetElements(elementsMap, appState);
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
);
let isSingleElementBoundContainer = false;
if (
@@ -137,12 +137,12 @@ export const SelectedShapeActions = ({
{renderAction("changeFontFamily")}
{(appState.activeTool.type === "text" ||
suppportsHorizontalAlign(targetElements, elementsMap)) &&
suppportsHorizontalAlign(targetElements)) &&
renderAction("changeTextAlign")}
</>
)}
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
{shouldAllowVerticalAlign(targetElements) &&
renderAction("changeVerticalAlign")}
{(canHaveArrowheads(appState.activeTool.type) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
+55 -99
View File
@@ -267,6 +267,7 @@ import {
isTransparent,
easeToValuesRAF,
muteFSAbortError,
arrayToMap,
isTestEnv,
easeOut,
updateStable,
@@ -348,8 +349,6 @@ import {
updateFrameMembershipOfSelectedElements,
isElementInFrame,
getFrameLikeTitle,
getElementsOverlappingFrame,
filterElementsEligibleAsFrameChildren,
} from "../frame";
import {
excludeElementsInFramesFromSelection,
@@ -397,7 +396,7 @@ import {
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
import { MagicCacheData, diagramToHTML } from "../data/magic";
import { exportToBlob } from "../../utils/export";
import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
@@ -1417,7 +1416,7 @@ class App extends React.Component<AppProps, AppState> {
const { renderTopRightUI, renderCustomStats } = this.props;
const versionNonce = this.scene.getVersionNonce();
const { elementsMap, visibleElements } =
const { canvasElements, visibleElements } =
this.renderer.getRenderableElements({
versionNonce,
zoom: this.state.zoom,
@@ -1431,8 +1430,6 @@ class App extends React.Component<AppProps, AppState> {
pendingImageElementId: this.state.pendingImageElementId,
});
const allElementsMap = this.scene.getNonDeletedElementsMap();
const shouldBlockPointerEvents =
!(
this.state.editingElement && isLinearElement(this.state.editingElement)
@@ -1629,8 +1626,7 @@ class App extends React.Component<AppProps, AppState> {
<StaticCanvas
canvas={this.canvas}
rc={this.rc}
elementsMap={elementsMap}
allElementsMap={allElementsMap}
elements={canvasElements}
visibleElements={visibleElements}
versionNonce={versionNonce}
selectionNonce={
@@ -1651,7 +1647,7 @@ class App extends React.Component<AppProps, AppState> {
<InteractiveCanvas
containerRef={this.excalidrawContainerRef}
canvas={this.interactiveCanvas}
elementsMap={elementsMap}
elements={canvasElements}
visibleElements={visibleElements}
selectedElements={selectedElements}
versionNonce={versionNonce}
@@ -1808,10 +1804,11 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const magicFrameChildren = getElementsOverlappingFrame(
this.scene.getNonDeletedElements(),
magicFrame,
).filter((el) => !isMagicFrameElement(el));
const magicFrameChildren = elementsOverlappingBBox({
elements: this.scene.getNonDeletedElements(),
bounds: magicFrame,
type: "overlap",
}).filter((el) => !isMagicFrameElement(el));
if (!magicFrameChildren.length) {
if (source === "button") {
@@ -2181,6 +2178,13 @@ class App extends React.Component<AppProps, AppState> {
},
);
}
// update frame membership if needed
updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
this,
);
},
);
@@ -2783,7 +2787,7 @@ class App extends React.Component<AppProps, AppState> {
private renderInteractiveSceneCallback = ({
atLeastOneVisibleElement,
scrollBars,
elementsMap,
elements,
}: RenderInteractiveSceneCallback) => {
if (scrollBars) {
currentScrollBars = scrollBars;
@@ -2792,7 +2796,7 @@ class App extends React.Component<AppProps, AppState> {
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && elementsMap.size > 0;
: !atLeastOneVisibleElement && elements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
@@ -3103,29 +3107,16 @@ class App extends React.Component<AppProps, AppState> {
},
);
const allElements = [
const nextElements = [
...this.scene.getElementsIncludingDeleted(),
...newElements,
];
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
if (topLayerFrame) {
const eligibleElements = filterElementsEligibleAsFrameChildren(
newElements,
topLayerFrame,
);
addElementsToFrame(allElements, eligibleElements, topLayerFrame);
}
this.scene.replaceAllElements(allElements);
this.scene.replaceAllElements(nextElements);
newElements.forEach((newElement) => {
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
const container = getContainerElement(
newElement,
this.scene.getElementsMapIncludingDeleted(),
);
const container = getContainerElement(newElement);
redrawTextBoundingBox(newElement, container);
}
});
@@ -3872,11 +3863,7 @@ class App extends React.Component<AppProps, AppState> {
if (!isTextElement(selectedElement)) {
container = selectedElement as ExcalidrawTextContainer;
}
const midPoint = getContainerCenter(
selectedElement,
this.state,
this.scene.getNonDeletedElementsMap(),
);
const midPoint = getContainerCenter(selectedElement, this.state);
const sceneX = midPoint.x;
const sceneY = midPoint.y;
this.startTextEditing({
@@ -4193,18 +4180,11 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement(
_element,
getContainerElement(
_element,
this.scene.getElementsMapIncludingDeleted(),
),
{
text,
isDeleted,
originalText,
},
);
return updateTextElement(_element, {
text,
isDeleted,
originalText,
});
}
return _element;
}),
@@ -4340,7 +4320,6 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
x,
y,
this.scene.getNonDeletedElementsMap(),
)
? allHitElements[allHitElements.length - 2]
: elementWithHighestZIndex;
@@ -4369,18 +4348,13 @@ class App extends React.Component<AppProps, AppState> {
!(isTextElement(element) && element.containerId)),
);
const elementsMap = arrayToMap(elements);
return getElementsAtPosition(elements, (element) =>
hitTest(
element,
this.state,
this.frameNameBoundsCache,
x,
y,
this.scene.getNonDeletedElementsMap(),
),
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element);
const containingFrame = getContainingFrame(element, elementsMap);
return containingFrame &&
this.state.frameRendering.enabled &&
this.state.frameRendering.clip
@@ -4414,10 +4388,7 @@ class App extends React.Component<AppProps, AppState> {
container,
);
if (container && parentCenterPosition) {
const boundTextElementToContainer = getBoundTextElement(
container,
this.scene.getNonDeletedElementsMap(),
);
const boundTextElementToContainer = getBoundTextElement(container);
if (!boundTextElementToContainer) {
shouldBindToContainer = true;
}
@@ -4430,10 +4401,7 @@ class App extends React.Component<AppProps, AppState> {
if (isTextElement(selectedElements[0])) {
existingTextElement = selectedElements[0];
} else if (container) {
existingTextElement = getBoundTextElement(
selectedElements[0],
this.scene.getNonDeletedElementsMap(),
);
existingTextElement = getBoundTextElement(selectedElements[0]);
} else {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
}
@@ -4642,11 +4610,7 @@ class App extends React.Component<AppProps, AppState> {
[sceneX, sceneY],
)
) {
const midPoint = getContainerCenter(
container,
this.state,
this.scene.getNonDeletedElementsMap(),
);
const midPoint = getContainerCenter(container, this.state);
sceneX = midPoint.x;
sceneY = midPoint.y;
@@ -5282,8 +5246,8 @@ class App extends React.Component<AppProps, AppState> {
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
);
const elementsMap = this.scene.getNonDeletedElementsMap();
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (!element) {
return;
@@ -5310,7 +5274,6 @@ class App extends React.Component<AppProps, AppState> {
linearElementEditor,
{ x: scenePointerX, y: scenePointerY },
this.state,
this.scene.getNonDeletedElementsMap(),
);
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
@@ -5326,7 +5289,6 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
scenePointerX,
scenePointerY,
elementsMap,
)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@@ -5338,7 +5300,6 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
scenePointerX,
scenePointerY,
this.scene.getNonDeletedElementsMap(),
)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@@ -5808,10 +5769,7 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault();
let nextPastePrevented = false;
const isLinux =
typeof window === undefined
? false
: /Linux/.test(window.navigator.platform);
const isLinux = /Linux/.test(window.navigator.platform);
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
let { clientX: lastX, clientY: lastY } = event;
@@ -6088,7 +6046,6 @@ class App extends React.Component<AppProps, AppState> {
this.history,
pointerDownState.origin,
linearElementEditor,
this.scene.getNonDeletedElementsMap(),
);
if (ret.hitElement) {
pointerDownState.hit.element = ret.hitElement;
@@ -7024,7 +6981,6 @@ class App extends React.Component<AppProps, AppState> {
);
},
linearElementEditor,
this.scene.getNonDeletedElementsMap(),
);
if (didDrag) {
pointerDownState.lastCoords.x = pointerCoords.x;
@@ -7733,7 +7689,10 @@ class App extends React.Component<AppProps, AppState> {
);
if (linearElement?.frameId) {
const frame = getContainingFrame(linearElement);
const frame = getContainingFrame(
linearElement,
arrayToMap(this.scene.getElementsIncludingDeleted()),
);
if (frame && linearElement) {
if (!elementOverlapsWithFrame(linearElement, frame)) {
@@ -7743,12 +7702,13 @@ class App extends React.Component<AppProps, AppState> {
groupIds: [],
});
removeElementsFromFrame(
[linearElement],
this.scene.getNonDeletedElementsMap(),
this.scene.replaceAllElements(
removeElementsFromFrame(
this.scene.getElementsIncludingDeleted(),
[linearElement],
this.state,
),
);
this.scene.informMutation();
}
}
}
@@ -7758,7 +7718,7 @@ class App extends React.Component<AppProps, AppState> {
this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = this.scene.getSelectedElements(this.state);
let nextElements = this.scene.getElementsMapIncludingDeleted();
let nextElements = this.scene.getElementsIncludingDeleted();
const updateGroupIdsAfterEditingGroup = (
elements: ExcalidrawElement[],
@@ -7851,7 +7811,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements(
addElementsToFrame(
this.scene.getElementsMapIncludingDeleted(),
this.scene.getElementsIncludingDeleted(),
elementsInsideFrame,
draggingElement,
),
@@ -7899,7 +7859,7 @@ class App extends React.Component<AppProps, AppState> {
this.state,
),
frame,
this,
this.state,
);
}
@@ -8127,7 +8087,6 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache,
pointerDownState.origin.x,
pointerDownState.origin.y,
this.scene.getNonDeletedElementsMap(),
)) ||
(!hitElement &&
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
@@ -9180,10 +9139,10 @@ class App extends React.Component<AppProps, AppState> {
if (
transformElements(
pointerDownState.originalElements,
pointerDownState,
transformHandleType,
selectedElements,
this.scene.getElementsMapIncludingDeleted(),
pointerDownState.resize.arrowDirection,
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),
selectedElements.length === 1 && isImageElement(selectedElements[0])
@@ -9193,6 +9152,7 @@ class App extends React.Component<AppProps, AppState> {
resizeY,
pointerDownState.resize.center.x,
pointerDownState.resize.center.y,
this.state,
)
) {
this.maybeSuggestBindingForAll(selectedElements);
@@ -9369,11 +9329,7 @@ class App extends React.Component<AppProps, AppState> {
let elementCenterX = container.x + container.width / 2;
let elementCenterY = container.y + container.height / 2;
const elementCenter = getContainerCenter(
container,
appState,
this.scene.getNonDeletedElementsMap(),
);
const elementCenter = getContainerCenter(container, appState);
if (elementCenter) {
elementCenterX = elementCenter.x;
elementCenterY = elementCenter.y;
+1 -1
View File
@@ -226,7 +226,7 @@ const LayerUI = ({
>
<SelectedShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
elements={elements}
renderAction={actionManager.renderAction}
/>
</Island>
@@ -183,7 +183,7 @@ export const MobileMenu = ({
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
elements={elements}
renderAction={actionManager.renderAction}
/>
</Section>
@@ -29,7 +29,6 @@
.default-sidebar-trigger .sidebar-trigger__label {
display: block;
white-space: nowrap;
}
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
@@ -51,12 +51,6 @@
color: var(--color-gray-100);
}
.UserList__collaborator-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.UserList__collaborator-follow-status-icon {
margin-left: auto;
flex: 0 0 auto;
@@ -7,7 +7,6 @@ import type { DOMAttributes } from "react";
import type { AppState, InteractiveCanvasAppState } from "../../types";
import type {
InteractiveCanvasRenderConfig,
RenderableElementsMap,
RenderInteractiveSceneCallback,
} from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
@@ -16,7 +15,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils";
type InteractiveCanvasProps = {
containerRef: React.RefObject<HTMLDivElement>;
canvas: HTMLCanvasElement | null;
elementsMap: RenderableElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
selectedElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
@@ -114,7 +113,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
renderInteractiveScene(
{
canvas: props.canvas,
elementsMap: props.elementsMap,
elements: props.elements,
visibleElements: props.visibleElements,
selectedElements: props.selectedElements,
scale: window.devicePixelRatio,
@@ -202,10 +201,10 @@ const areEqual = (
prevProps.selectionNonce !== nextProps.selectionNonce ||
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on elementsMap because they may have renewed
// we need to memoize on element arrays because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.elements !== nextProps.elements ||
prevProps.visibleElements !== nextProps.visibleElements ||
prevProps.selectedElements !== nextProps.selectedElements
) {
@@ -3,21 +3,14 @@ import { RoughCanvas } from "roughjs/bin/canvas";
import { renderStaticScene } from "../../renderer/renderScene";
import { isShallowEqual } from "../../utils";
import type { AppState, StaticCanvasAppState } from "../../types";
import type {
RenderableElementsMap,
StaticCanvasRenderConfig,
} from "../../scene/types";
import type {
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../../element/types";
import type { StaticCanvasRenderConfig } from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
import { isRenderThrottlingEnabled } from "../../reactUtils";
type StaticCanvasProps = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elementsMap: RenderableElementsMap;
allElementsMap: NonDeletedSceneElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
selectionNonce: number | undefined;
@@ -70,8 +63,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
canvas,
rc: props.rc,
scale: props.scale,
elementsMap: props.elementsMap,
allElementsMap: props.allElementsMap,
elements: props.elements,
visibleElements: props.visibleElements,
appState: props.appState,
renderConfig: props.renderConfig,
@@ -114,10 +106,10 @@ const areEqual = (
if (
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on elementsMap because they may have renewed
// we need to memoize on element arrays because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.elements !== nextProps.elements ||
prevProps.visibleElements !== nextProps.visibleElements
) {
return false;
+1
View File
@@ -2,6 +2,7 @@ import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
import { COLOR_PALETTE } from "./colors";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform);
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
+6 -2
View File
@@ -11,6 +11,7 @@ import {
NonDeletedExcalidrawElement,
} from "../element/types";
import { t } from "../i18n";
import { elementsOverlappingBBox } from "../../utils/export";
import { isSomeElementSelected, getSelectedElements } from "../scene";
import { exportToCanvas, exportToSvg } from "../scene/export";
import { ExportType } from "../scene/types";
@@ -19,7 +20,6 @@ import { cloneJSON } from "../utils";
import { canvasToBlob } from "./blob";
import { fileSave, FileSystemHandle } from "./filesystem";
import { serializeAsJSON } from "./json";
import { getElementsOverlappingFrame } from "../frame";
export { loadFromBlob } from "./blob";
export { loadFromJSON, saveAsJSON } from "./json";
@@ -56,7 +56,11 @@ export const prepareElementsForExport = (
isFrameLikeElement(exportedElements[0])
) {
exportingFrame = exportedElements[0];
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
exportedElements = elementsOverlappingBBox({
elements,
bounds: exportingFrame,
type: "overlap",
});
} else if (exportedElements.length > 1) {
exportedElements = getSelectedElements(
elements,
+9 -12
View File
@@ -40,7 +40,6 @@ import { arrayToMap } from "../utils";
import { MarkOptional, Mutable } from "../utility-types";
import {
detectLineHeight,
getContainerElement,
getDefaultLineHeight,
measureBaseline,
} from "../element/textElement";
@@ -180,6 +179,7 @@ const restoreElementWithProperties = <
const restoreElement = (
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
refreshDimensions = false,
): typeof element | null => {
switch (element.type) {
case "text":
@@ -232,6 +232,10 @@ const restoreElement = (
element = bumpVersion(element);
}
if (refreshDimensions) {
element = { ...element, ...refreshTextDimensions(element) };
}
return element;
case "freedraw": {
return restoreElementWithProperties(element, {
@@ -422,7 +426,10 @@ export const restoreElements = (
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(element);
let migratedElement: ExcalidrawElement | null = restoreElement(
element,
opts?.refreshDimensions,
);
if (migratedElement) {
const localElement = localElementsMap?.get(element.id);
if (localElement && localElement.version > migratedElement.version) {
@@ -455,16 +462,6 @@ export const restoreElements = (
} else if (element.boundElements) {
repairContainerElement(element, restoredElementsMap);
}
if (opts.refreshDimensions && isTextElement(element)) {
Object.assign(
element,
refreshTextDimensions(
element,
getContainerElement(element, restoredElementsMap),
),
);
}
}
return restoredElements;
+1 -4
View File
@@ -24,7 +24,6 @@ import {
normalizeText,
} from "../element/textElement";
import {
ElementsMap,
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElement,
@@ -43,7 +42,7 @@ import {
VerticalAlign,
} from "../element/types";
import { MarkOptional } from "../utility-types";
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
import { assertNever, cloneJSON, getFontString } from "../utils";
import { getSizeFromPoints } from "../points";
import { randomId } from "../random";
@@ -203,7 +202,6 @@ const DEFAULT_DIMENSION = 100;
const bindTextToContainer = (
container: ExcalidrawElement,
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
elementsMap: ElementsMap,
) => {
const textElement: ExcalidrawTextElement = newTextElement({
x: 0,
@@ -625,7 +623,6 @@ export const convertToExcalidrawElements = (
let [container, text] = bindTextToContainer(
excalidrawElement,
element?.label,
arrayToMap(elementStore.getElements()),
);
elementStore.add(container);
elementStore.add(text);
+2 -3
View File
@@ -1,7 +1,7 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { getMaximumGroups } from "./groups";
import { getCommonBoundingBox } from "./element/bounds";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
export interface Distribution {
space: "between";
@@ -10,7 +10,6 @@ export interface Distribution {
export const distributeElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
distribution: Distribution,
): ExcalidrawElement[] => {
const [start, mid, end, extent] =
@@ -19,7 +18,7 @@ export const distributeElements = (
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements, elementsMap)
const groups = getMaximumGroups(selectedElements)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.sort((a, b) => a[1][mid] - b[1][mid]);
+4 -7
View File
@@ -321,9 +321,9 @@ export const updateBoundElements = (
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated,
);
const scene = Scene.getScene(changedElement)!;
getNonDeletedElements(
scene,
Scene.getScene(changedElement)!,
boundLinearElements.map((el) => el.id),
).forEach((element) => {
if (!isLinearElement(element)) {
@@ -362,12 +362,9 @@ export const updateBoundElements = (
endBinding,
changedElement as ExcalidrawBindableElement,
);
const boundText = getBoundTextElement(
element,
scene.getNonDeletedElementsMap(),
);
const boundText = getBoundTextElement(element);
if (boundText) {
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
handleBindTextResize(element, false);
}
});
};
+12 -24
View File
@@ -5,8 +5,6 @@ import {
ExcalidrawFreeDrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMapOrArray,
ElementsMap,
} from "./types";
import { distance2d, rotate, rotatePoint } from "../math";
import rough from "roughjs/bin/rough";
@@ -75,16 +73,13 @@ export class ElementBounds {
) {
return cachedBounds.bounds;
}
const scene = Scene.getScene(element);
const bounds = ElementBounds.calculateBounds(
element,
scene?.getNonDeletedElementsMap() || new Map(),
);
const bounds = ElementBounds.calculateBounds(element);
// hack to ensure that downstream checks could retrieve element Scene
// so as to have correctly calculated bounds
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
const shouldCache = !!scene;
const shouldCache = Scene.getScene(element);
if (shouldCache) {
ElementBounds.boundsCache.set(element, {
@@ -96,10 +91,7 @@ export class ElementBounds {
return bounds;
}
private static calculateBounds(
element: ExcalidrawElement,
elementsMap: ElementsMap,
): Bounds {
private static calculateBounds(element: ExcalidrawElement): Bounds {
let bounds: Bounds;
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
@@ -118,7 +110,7 @@ export class ElementBounds {
maxY + element.y,
];
} else if (isLinearElement(element)) {
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
bounds = getLinearElementRotatedBounds(element, cx, cy);
} else if (element.type === "diamond") {
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
@@ -161,20 +153,15 @@ export const getElementAbsoluteCoords = (
element: ExcalidrawElement,
includeBoundText: boolean = false,
): [number, number, number, number, number, number] => {
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
if (isFreeDrawElement(element)) {
return getFreeDrawElementAbsoluteCoords(element);
} else if (isLinearElement(element)) {
return LinearElementEditor.getElementAbsoluteCoords(
element,
elementsMap,
includeBoundText,
);
} else if (isTextElement(element)) {
const container = elementsMap
? getContainerElement(element, elementsMap)
: null;
const container = getContainerElement(element);
if (isArrowElement(container)) {
const coords = LinearElementEditor.getBoundTextElementPosition(
container,
@@ -685,10 +672,7 @@ const getLinearElementRotatedBounds = (
element: ExcalidrawLinearElement,
cx: number,
cy: number,
elementsMap: ElementsMap,
): Bounds => {
const boundTextElement = getBoundTextElement(element, elementsMap);
if (element.points.length < 2) {
const [pointX, pointY] = element.points[0];
const [x, y] = rotate(
@@ -700,6 +684,7 @@ const getLinearElementRotatedBounds = (
);
let coords: Bounds = [x, y, x, y];
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element,
@@ -724,6 +709,7 @@ const getLinearElementRotatedBounds = (
rotate(element.x + x, element.y + y, cx, cy, element.angle);
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
let coords: Bounds = [res[0], res[1], res[2], res[3]];
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element,
@@ -743,8 +729,10 @@ const getLinearElementRotatedBounds = (
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
return ElementBounds.getBounds(element);
};
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
if ("size" in elements ? !elements.size : !elements.length) {
export const getCommonBounds = (
elements: readonly ExcalidrawElement[],
): Bounds => {
if (!elements.length) {
return [0, 0, 0, 0];
}
+3 -7
View File
@@ -28,7 +28,6 @@ import {
StrokeRoundness,
ExcalidrawFrameLikeElement,
ExcalidrawIframeLikeElement,
ElementsMap,
} from "./types";
import {
@@ -79,7 +78,6 @@ export const hitTest = (
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
elementsMap: ElementsMap,
): boolean => {
// How many pixels off the shape boundary we still consider a hit
const threshold = 10 / appState.zoom.value;
@@ -97,7 +95,7 @@ export const hitTest = (
);
}
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const isHittingBoundTextElement = hitTest(
boundTextElement,
@@ -105,7 +103,6 @@ export const hitTest = (
frameNameBoundsCache,
x,
y,
elementsMap,
);
if (isHittingBoundTextElement) {
return true;
@@ -125,16 +122,15 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
elementsMap: ElementsMap,
): boolean => {
const threshold = 10 / appState.zoom.value;
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
// eg for linear elements text can be outside the element bounding box
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (
boundTextElement &&
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
) {
return false;
}
+1 -4
View File
@@ -57,10 +57,7 @@ export const dragSelectedElements = (
// skip arrow labels since we calculate its position during render
!isArrowElement(element)
) {
const textElement = getBoundTextElement(
element,
scene.getNonDeletedElementsMap(),
);
const textElement = getBoundTextElement(element);
if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset);
}
+22 -2
View File
@@ -5,12 +5,17 @@ import { ExcalidrawProps } from "../types";
import { getFontString, updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { newTextElement } from "./newElement";
import { wrapText } from "./textElement";
import { isIframeElement } from "./typeChecks";
import { getContainerElement, wrapText } from "./textElement";
import {
isFrameLikeElement,
isIframeElement,
isIframeLikeElement,
} from "./typeChecks";
import {
ExcalidrawElement,
ExcalidrawIframeLikeElement,
IframeData,
NonDeletedExcalidrawElement,
} from "./types";
const embeddedLinkCache = new Map<string, IframeData>();
@@ -212,6 +217,21 @@ export const getEmbedLink = (
return { link, intrinsicSize: aspectRatio, type };
};
export const isIframeLikeOrItsLabel = (
element: NonDeletedExcalidrawElement,
): Boolean => {
if (isIframeLikeElement(element)) {
return true;
}
if (element.type === "text") {
const container = getContainerElement(element);
if (container && isFrameLikeElement(container)) {
return true;
}
}
return false;
};
export const createPlaceholderEmbeddableLabel = (
element: ExcalidrawIframeLikeElement,
): ExcalidrawElement => {
@@ -5,7 +5,6 @@ import {
PointBinding,
ExcalidrawBindableElement,
ExcalidrawTextElementWithContainer,
ElementsMap,
} from "./types";
import {
distance2d,
@@ -194,7 +193,6 @@ export class LinearElementEditor {
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap,
): boolean {
if (!linearElementEditor) {
return false;
@@ -274,9 +272,9 @@ export class LinearElementEditor {
);
}
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
handleBindTextResize(element, elementsMap, false);
handleBindTextResize(element, false);
}
// suggest bindings for first and last point if selected
@@ -406,10 +404,9 @@ export class LinearElementEditor {
static getEditorMidPoints = (
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
appState: InteractiveCanvasAppState,
): typeof editorMidPointsCache["points"] => {
const boundText = getBoundTextElement(element, elementsMap);
const boundText = getBoundTextElement(element);
// Since its not needed outside editor unless 2 pointer lines or bound text
if (
@@ -468,7 +465,6 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number },
appState: AppState,
elementsMap: ElementsMap,
) => {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
@@ -507,7 +503,7 @@ export class LinearElementEditor {
}
let index = 0;
const midPoints: typeof editorMidPointsCache["points"] =
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
LinearElementEditor.getEditorMidPoints(element, appState);
while (index < midPoints.length) {
if (midPoints[index] !== null) {
const distance = distance2d(
@@ -585,7 +581,6 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor,
appState: AppState,
midPoint: Point,
elementsMap: ElementsMap,
) {
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
@@ -593,11 +588,7 @@ export class LinearElementEditor {
if (!element) {
return -1;
}
const midPoints = LinearElementEditor.getEditorMidPoints(
element,
elementsMap,
appState,
);
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
let index = 0;
while (index < midPoints.length) {
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
@@ -614,7 +605,6 @@ export class LinearElementEditor {
history: History,
scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap,
): {
didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null;
@@ -640,7 +630,6 @@ export class LinearElementEditor {
linearElementEditor,
scenePointer,
appState,
elementsMap,
);
let segmentMidpointIndex = null;
if (segmentMidpoint) {
@@ -648,7 +637,6 @@ export class LinearElementEditor {
linearElementEditor,
appState,
segmentMidpoint,
elementsMap,
);
}
if (event.altKey && appState.editingLinearElement) {
@@ -1430,7 +1418,6 @@ export class LinearElementEditor {
static getElementAbsoluteCoords = (
element: ExcalidrawLinearElement,
elementsMap: ElementsMap,
includeBoundText: boolean = false,
): [number, number, number, number, number, number] => {
let coords: [number, number, number, number, number, number];
@@ -1475,7 +1462,7 @@ export class LinearElementEditor {
if (!includeBoundText) {
return coords;
}
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
coords = LinearElementEditor.getMinMaxXYWithBoundText(
element,
+4 -4
View File
@@ -31,6 +31,7 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import {
getContainerElement,
measureText,
normalizeText,
wrapText,
@@ -332,17 +333,17 @@ const getAdjustedDimensions = (
export const refreshTextDimensions = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
text = textElement.text,
) => {
if (textElement.isDeleted) {
return;
}
const container = getContainerElement(textElement);
if (container) {
text = wrapText(
text,
getFontString(textElement),
getBoundTextMaxWidth(container, textElement),
getBoundTextMaxWidth(container),
);
}
const dimensions = getAdjustedDimensions(textElement, text);
@@ -351,7 +352,6 @@ export const refreshTextDimensions = (
export const updateTextElement = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
{
text,
isDeleted,
@@ -365,7 +365,7 @@ export const updateTextElement = (
return newElementWith(textElement, {
originalText,
isDeleted: isDeleted ?? textElement.isDeleted,
...refreshTextDimensions(textElement, container, originalText),
...refreshTextDimensions(textElement, originalText),
});
};
+25 -43
View File
@@ -15,7 +15,6 @@ import {
ExcalidrawElement,
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
ElementsMap,
} from "./types";
import type { Mutable } from "../utility-types";
import {
@@ -42,7 +41,7 @@ import {
MaybeTransformHandleType,
TransformHandleDirection,
} from "./transformHandles";
import { Point, PointerDownState } from "../types";
import { AppState, Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineWidth,
@@ -69,10 +68,10 @@ export const normalizeAngle = (angle: number): number => {
// Returns true when transform (resizing/rotation) happened
export const transformElements = (
originalElements: PointerDownState["originalElements"],
pointerDownState: PointerDownState,
transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
resizeArrowDirection: "origin" | "end",
shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean,
@@ -80,6 +79,7 @@ export const transformElements = (
pointerY: number,
centerX: number,
centerY: number,
appState: AppState,
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@@ -89,6 +89,7 @@ export const transformElements = (
pointerX,
pointerY,
shouldRotateWithDiscreteAngle,
pointerDownState.originalElements,
);
updateBoundElements(element);
} else if (
@@ -100,7 +101,6 @@ export const transformElements = (
) {
resizeSingleTextElement(
element,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@@ -109,10 +109,9 @@ export const transformElements = (
updateBoundElements(element);
} else if (transformHandleType) {
resizeSingleElement(
originalElements,
pointerDownState.originalElements,
shouldMaintainAspectRatio,
element,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@@ -124,9 +123,8 @@ export const transformElements = (
} else if (selectedElements.length > 1) {
if (transformHandleType === "rotation") {
rotateMultipleElements(
originalElements,
pointerDownState,
selectedElements,
elementsMap,
pointerX,
pointerY,
shouldRotateWithDiscreteAngle,
@@ -141,9 +139,8 @@ export const transformElements = (
transformHandleType === "se"
) {
resizeMultipleElements(
originalElements,
pointerDownState,
selectedElements,
elementsMap,
transformHandleType,
shouldResizeFromCenter,
pointerX,
@@ -160,6 +157,7 @@ const rotateSingleElement = (
pointerX: number,
pointerY: number,
shouldRotateWithDiscreteAngle: boolean,
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
@@ -209,7 +207,6 @@ const rescalePointsInElement = (
const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
nextWidth: number,
nextHeight: number,
): { size: number; baseline: number } | null => {
@@ -218,9 +215,9 @@ const measureFontSizeFromWidth = (
const hasContainer = isBoundToContainer(element);
if (hasContainer) {
const container = getContainerElement(element, elementsMap);
const container = getContainerElement(element);
if (container) {
width = getBoundTextMaxWidth(container, element);
width = getBoundTextMaxWidth(container);
}
}
const nextFontSize = element.fontSize * (nextWidth / width);
@@ -260,7 +257,6 @@ const getSidesForTransformHandle = (
const resizeSingleTextElement = (
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
@@ -307,12 +303,7 @@ const resizeSingleTextElement = (
if (scale > 0) {
const nextWidth = element.width * scale;
const nextHeight = element.height * scale;
const metrics = measureFontSizeFromWidth(
element,
elementsMap,
nextWidth,
nextHeight,
);
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
if (metrics === null) {
return;
}
@@ -351,7 +342,6 @@ export const resizeSingleElement = (
originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
transformHandleDirection: TransformHandleDirection,
shouldResizeFromCenter: boolean,
pointerX: number,
@@ -395,7 +385,7 @@ export const resizeSingleElement = (
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) {
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
@@ -458,8 +448,7 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
elementsMap,
getBoundTextMaxWidth(updatedElement, boundTextElement),
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
);
if (nextFont === null) {
@@ -641,7 +630,6 @@ export const resizeSingleElement = (
}
handleBindTextResize(
element,
elementsMap,
transformHandleDirection,
shouldMaintainAspectRatio,
);
@@ -649,9 +637,8 @@ export const resizeSingleElement = (
};
export const resizeMultipleElements = (
originalElements: PointerDownState["originalElements"],
pointerDownState: PointerDownState,
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
@@ -671,7 +658,7 @@ export const resizeMultipleElements = (
}[],
element,
) => {
const origElement = originalElements.get(element.id);
const origElement = pointerDownState.originalElements.get(element.id);
if (origElement) {
acc.push({ orig: origElement, latest: element });
}
@@ -692,7 +679,7 @@ export const resizeMultipleElements = (
if (!textId) {
return acc;
}
const text = originalElements.get(textId) ?? null;
const text = pointerDownState.originalElements.get(textId) ?? null;
if (!isBoundToContainer(text)) {
return acc;
}
@@ -838,12 +825,7 @@ export const resizeMultipleElements = (
}
if (isTextElement(orig)) {
const metrics = measureFontSizeFromWidth(
orig,
elementsMap,
width,
height,
);
const metrics = measureFontSizeFromWidth(orig, width, height);
if (!metrics) {
return;
}
@@ -851,7 +833,7 @@ export const resizeMultipleElements = (
update.baseline = metrics.baseline;
}
const boundTextElement = originalElements.get(
const boundTextElement = pointerDownState.originalElements.get(
getBoundTextElementId(orig) ?? "",
) as ExcalidrawTextElementWithContainer | undefined;
@@ -884,7 +866,7 @@ export const resizeMultipleElements = (
newSize: { width, height },
});
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement && boundTextFontSize) {
mutateElement(
boundTextElement,
@@ -894,7 +876,7 @@ export const resizeMultipleElements = (
},
false,
);
handleBindTextResize(element, elementsMap, transformHandleType, true);
handleBindTextResize(element, transformHandleType, true);
}
}
@@ -902,9 +884,8 @@ export const resizeMultipleElements = (
};
const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"],
pointerDownState: PointerDownState,
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
pointerX: number,
pointerY: number,
shouldRotateWithDiscreteAngle: boolean,
@@ -925,7 +906,8 @@ const rotateMultipleElements = (
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const origAngle =
originalElements.get(element.id)?.angle ?? element.angle;
pointerDownState.originalElements.get(element.id)?.angle ??
element.angle;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
@@ -944,7 +926,7 @@ const rotateMultipleElements = (
);
updateBoundElements(element, { simultaneouslyUpdated: elements });
const boundText = getBoundTextElement(element, elementsMap);
const boundText = getBoundTextElement(element);
if (boundText && !isArrowElement(element)) {
mutateElement(
boundText,
@@ -319,17 +319,17 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getBoundTextMaxWidth(container, null)).toBe(168);
expect(getBoundTextMaxWidth(container)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getBoundTextMaxWidth(container, null)).toBe(116);
expect(getBoundTextMaxWidth(container)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getBoundTextMaxWidth(container, null)).toBe(79);
expect(getBoundTextMaxWidth(container)).toBe(79);
});
});
+58 -35
View File
@@ -1,6 +1,5 @@
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
import {
ElementsMap,
ExcalidrawElement,
ExcalidrawElementType,
ExcalidrawTextContainer,
@@ -23,6 +22,7 @@ import {
VERTICAL_ALIGN,
} from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { isTextElement } from ".";
import { isBoundToContainer, isArrowElement } from "./typeChecks";
import { LinearElementEditor } from "./linearElementEditor";
@@ -88,7 +88,7 @@ export const redrawTextBoundingBox = (
container,
textElement as ExcalidrawTextElementWithContainer,
);
const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
const nextHeight = computeContainerDimensionForBoundText(
@@ -161,7 +161,6 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = (
container: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => {
@@ -170,17 +169,25 @@ export const handleBindTextResize = (
return;
}
resetOriginalContainerCache(container.id);
const textElement = getBoundTextElement(container, elementsMap);
let textElement = Scene.getScene(container)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (textElement && textElement.text) {
if (!container) {
return;
}
textElement = Scene.getScene(container)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
let text = textElement.text;
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const maxWidth = getBoundTextMaxWidth(container, textElement);
const maxHeight = getBoundTextMaxHeight(container, textElement);
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = container.height;
let nextBaseLine = textElement.baseline;
if (
@@ -235,7 +242,10 @@ export const handleBindTextResize = (
if (!isArrowElement(container)) {
mutateElement(
textElement,
computeBoundTextPosition(container, textElement),
computeBoundTextPosition(
container,
textElement as ExcalidrawTextElementWithContainer,
),
);
}
}
@@ -253,7 +263,7 @@ export const computeBoundTextPosition = (
}
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
let x;
let y;
@@ -656,32 +666,33 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
: null;
};
export const getBoundTextElement = (
element: ExcalidrawElement | null,
elementsMap: ElementsMap,
) => {
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
if (!element) {
return null;
}
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
return (elementsMap.get(boundTextElementId) ||
null) as ExcalidrawTextElementWithContainer | null;
return (
(Scene.getScene(element)?.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer) || null
);
}
return null;
};
export const getContainerElement = (
element: ExcalidrawTextElement | null,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null => {
element:
| (ExcalidrawElement & {
containerId: ExcalidrawElement["id"] | null;
})
| null,
) => {
if (!element) {
return null;
}
if (element.containerId) {
return (elementsMap.get(element.containerId) ||
null) as ExcalidrawTextContainer | null;
return Scene.getScene(element)?.getElement(element.containerId) || null;
}
return null;
};
@@ -689,7 +700,6 @@ export const getContainerElement = (
export const getContainerCenter = (
container: ExcalidrawElement,
appState: AppState,
elementsMap: ElementsMap,
) => {
if (!isArrowElement(container)) {
return {
@@ -709,7 +719,6 @@ export const getContainerCenter = (
const index = container.points.length / 2 - 1;
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
container,
elementsMap,
appState,
)[index];
if (!midSegmentMidpoint) {
@@ -743,16 +752,28 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
};
};
export const getTextElementAngle = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
) => {
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
const container = getContainerElement(textElement);
if (!container || isArrowElement(container)) {
return textElement.angle;
}
return container.angle;
};
export const getBoundTextElementOffset = (
boundTextElement: ExcalidrawTextElement | null,
) => {
const container = getContainerElement(boundTextElement);
if (!container || !boundTextElement) {
return 0;
}
if (isArrowElement(container)) {
return BOUND_TEXT_PADDING * 8;
}
return BOUND_TEXT_PADDING;
};
export const getBoundTextElementPosition = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
@@ -767,12 +788,12 @@ export const getBoundTextElementPosition = (
export const shouldAllowVerticalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
) => {
return selectedElements.some((element) => {
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
return false;
}
return true;
@@ -783,12 +804,12 @@ export const shouldAllowVerticalAlign = (
export const suppportsHorizontalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
) => {
return selectedElements.some((element) => {
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
return false;
}
return true;
@@ -869,7 +890,9 @@ export const computeContainerDimensionForBoundText = (
export const getBoundTextMaxWidth = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElement | null,
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
container,
),
) => {
const { width } = container;
if (isArrowElement(container)) {
+10 -23
View File
@@ -34,14 +34,15 @@ import {
computeContainerDimensionForBoundText,
detectLineHeight,
computeBoundTextPosition,
getBoundTextElement,
} from "./textElement";
import { actionDecreaseFontSize } from "../actions/actionDecreaseFontSize";
import {
actionDecreaseFontSize,
actionIncreaseFontSize,
} from "../actions/actionProperties";
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App";
import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard";
import { actionIncreaseFontSize } from "../actions";
const getTransform = (
width: number,
@@ -152,10 +153,7 @@ export const textWysiwyg = ({
if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y;
const container = getContainerElement(
updatedTextElement,
app.scene.getElementsMapIncludingDeleted(),
);
const container = getContainerElement(updatedTextElement);
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
@@ -195,8 +193,7 @@ export const textWysiwyg = ({
}
}
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
maxWidth = getBoundTextMaxWidth(container);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
@@ -280,7 +277,7 @@ export const textWysiwyg = ({
transform: getTransform(
textElementWidth,
textElementHeight,
getTextElementAngle(updatedTextElement, container),
getTextElementAngle(updatedTextElement),
appState,
maxWidth,
editorMaxHeight,
@@ -351,24 +348,17 @@ export const textWysiwyg = ({
if (!data) {
return;
}
const container = getContainerElement(
element,
app.scene.getElementsMapIncludingDeleted(),
);
const container = getContainerElement(element);
const font = getFontString({
fontSize: app.state.currentItemFontSize,
fontFamily: app.state.currentItemFontFamily,
});
if (container) {
const boundTextElement = getBoundTextElement(
container,
app.scene.getNonDeletedElementsMap(),
);
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getBoundTextMaxWidth(container, boundTextElement),
getBoundTextMaxWidth(container),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
@@ -538,10 +528,7 @@ export const textWysiwyg = ({
return;
}
let text = editable.value;
const container = getContainerElement(
updateElement,
app.scene.getElementsMapIncludingDeleted(),
);
const container = getContainerElement(updateElement);
if (container) {
text = updateElement.text;
+1 -39
View File
@@ -6,7 +6,7 @@ import {
THEME,
VERTICAL_ALIGN,
} from "../constants";
import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
import { MarkNonNullable, ValueOf } from "../utility-types";
import { MagicCacheData } from "../data/magic";
export type ChartType = "bar" | "line";
@@ -254,41 +254,3 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
export type FileId = string & { _brand: "FileId" };
export type ExcalidrawElementType = ExcalidrawElement["type"];
/**
* Map of excalidraw elements.
* Unspecified whether deleted or non-deleted.
* Can be a subset of Scene elements.
*/
export type ElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement>;
/**
* Map of non-deleted elements.
* Can be a subset of Scene elements.
*/
export type NonDeletedElementsMap = Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
> &
MakeBrand<"NonDeletedElementsMap">;
/**
* Map of all excalidraw Scene elements, including deleted.
* Not a subset. Use this type when you need access to current Scene elements.
*/
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
MakeBrand<"SceneElementsMap">;
/**
* Map of all non-deleted Scene elements.
* Not a subset. Use this type when you need access to current Scene elements.
*/
export type NonDeletedSceneElementsMap = Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
> &
MakeBrand<"NonDeletedSceneElementsMap">;
export type ElementsMapOrArray =
| readonly ExcalidrawElement[]
| Readonly<ElementsMap>;
@@ -15,23 +15,14 @@
border-radius: 50%;
}
}
.app-title {
margin-block-start: 0.83em;
margin-block-end: 0.83em;
}
}
.button-wrapper {
input[type="checkbox"] {
margin: 5px;
}
button {
z-index: 1;
height: 40px;
max-width: 200px;
margin: 10px;
padding: 5px;
}
.button-wrapper button {
z-index: 1;
height: 40px;
max-width: 200px;
margin: 10px;
padding: 5px;
}
.excalidraw .App-menu_top .buttonList {
@@ -1,30 +1,15 @@
import React, {
useEffect,
useState,
useRef,
useCallback,
Children,
cloneElement,
} from "react";
import ExampleSidebar from "./sidebar/ExampleSidebar";
import type * as TExcalidraw from "@excalidraw/excalidraw";
import type * as TExcalidraw from "../index";
import "./App.scss";
import initialData from "./initialData";
import { nanoid } from "nanoid";
import {
resolvablePromise,
ResolvablePromise,
distance2d,
fileOpen,
withBatchedUpdates,
withBatchedUpdatesThrottled,
} from "../utils";
import CustomFooter from "./CustomFooter";
import MobileFooter from "./MobileFooter";
import initialData from "../initialData";
import { resolvablePromise, ResolvablePromise } from "../utils";
import { EVENT, ROUNDNESS } from "../constants";
import { distance2d } from "../math";
import { fileOpen } from "../data/filesystem";
import { loadSceneOrLibraryFromBlob } from "../../utils";
import type {
AppState,
BinaryFileData,
@@ -33,14 +18,19 @@ import type {
Gesture,
LibraryItems,
PointerDownState as ExcalidrawPointerDownState,
} from "@excalidraw/excalidraw/dist/excalidraw/types";
import type {
NonDeletedExcalidrawElement,
Theme,
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
} from "../types";
import type { NonDeletedExcalidrawElement, Theme } from "../element/types";
import { ImportedLibraryData } from "../data/types";
import CustomFooter from "./CustomFooter";
import MobileFooter from "./MobileFooter";
import { KEYS } from "../keys";
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
import "./App.scss";
declare global {
interface Window {
ExcalidrawLib: typeof TExcalidraw;
}
}
type Comment = {
x: number;
@@ -61,6 +51,31 @@ type PointerDownState = {
};
};
const { useEffect, useState, useRef, useCallback } = window.React;
// This is so that we use the bundled excalidraw.development.js file instead
// of the actual source code
const {
exportToCanvas,
exportToSvg,
exportToBlob,
exportToClipboard,
Excalidraw,
useHandleLibrary,
MIME_TYPES,
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
restoreElements,
Sidebar,
Footer,
WelcomeScreen,
MainMenu,
LiveCollaborationTrigger,
convertToExcalidrawElements,
TTDDialog,
TTDDialogTrigger,
} = window.ExcalidrawLib;
const COMMENT_ICON_DIMENSION = 32;
const COMMENT_INPUT_HEIGHT = 50;
const COMMENT_INPUT_WIDTH = 150;
@@ -69,38 +84,8 @@ export interface AppProps {
appTitle: string;
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
customArgs?: any[];
children: React.ReactNode;
excalidrawLib: typeof TExcalidraw;
}
export default function App({
appTitle,
useCustom,
customArgs,
children,
excalidrawLib,
}: AppProps) {
const {
exportToCanvas,
exportToSvg,
exportToBlob,
exportToClipboard,
useHandleLibrary,
MIME_TYPES,
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
restoreElements,
Sidebar,
Footer,
WelcomeScreen,
MainMenu,
LiveCollaborationTrigger,
convertToExcalidrawElements,
TTDDialog,
TTDDialogTrigger,
ROUNDNESS,
loadSceneOrLibraryFromBlob,
} = excalidrawLib;
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
const appRef = useRef<any>(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
@@ -162,105 +147,8 @@ export default function App({
};
};
fetchData();
}, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
}, [excalidrawAPI]);
const renderExcalidraw = (children: React.ReactNode) => {
const Excalidraw: any = Children.toArray(children).find(
(child) =>
React.isValidElement(child) &&
typeof child.type !== "string" &&
//@ts-ignore
child.type.displayName === "Excalidraw",
);
if (!Excalidraw) {
return;
}
const newElement = cloneElement(
Excalidraw,
{
excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
initialData: initialStatePromiseRef.current.promise,
onChange: (
elements: NonDeletedExcalidrawElement[],
state: AppState,
) => {
console.info("Elements :", elements, "State : ", state);
},
onPointerUpdate: (payload: {
pointer: { x: number; y: number };
button: "down" | "up";
pointersMap: Gesture["pointers"];
}) => setPointerData(payload),
viewModeEnabled,
zenModeEnabled,
gridModeEnabled,
theme,
name: "Custom name of drawing",
UIOptions: {
canvasActions: {
loadScene: false,
},
tools: { image: !disableImageTool },
},
renderTopRightUI,
onLinkOpen,
onPointerDown,
onScrollChange: rerenderCommentIcons,
validateEmbeddable: true,
},
<>
{excalidrawAPI && (
<Footer>
<CustomFooter
excalidrawAPI={excalidrawAPI}
excalidrawLib={excalidrawLib}
/>
</Footer>
)}
<WelcomeScreen />
<Sidebar name="custom">
<Sidebar.Tabs>
<Sidebar.Header />
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
<Sidebar.TabTriggers>
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
</Sidebar.TabTriggers>
</Sidebar.Tabs>
</Sidebar>
<Sidebar.Trigger
name="custom"
tab="one"
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
bottom: "20px",
zIndex: 9999999999999999,
}}
>
Toggle Custom Sidebar
</Sidebar.Trigger>
{renderMenu()}
{excalidrawAPI && (
<TTDDialogTrigger icon={<span>😀</span>}>
Text to diagram
</TTDDialogTrigger>
)}
<TTDDialog
onTextSubmit={async (_) => {
console.info("submit");
// sleep for 2s
await new Promise((resolve) => setTimeout(resolve, 2000));
throw new Error("error, go away now");
// return "dummy";
}}
/>
</>,
);
return newElement;
};
const renderTopRightUI = (isMobile: boolean) => {
return (
<>
@@ -444,8 +332,8 @@ export default function App({
pointerDownState: PointerDownState,
) => {
return withBatchedUpdates((event) => {
window.removeEventListener("pointermove", pointerDownState.onMove);
window.removeEventListener("pointerup", pointerDownState.onUp);
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
excalidrawAPI?.setActiveTool({ type: "selection" });
const distance = distance2d(
pointerDownState.x,
@@ -509,8 +397,8 @@ export default function App({
onPointerMoveFromPointerDownHandler(pointerDownState);
const onPointerUp =
onPointerUpFromPointerDownHandler(pointerDownState);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
pointerDownState.onMove = onPointerMove;
pointerDownState.onUp = onPointerUp;
@@ -602,7 +490,7 @@ export default function App({
}}
onBlur={saveComment}
onKeyDown={(event) => {
if (!event.shiftKey && event.key === "Enter") {
if (!event.shiftKey && event.key === KEYS.ENTER) {
event.preventDefault();
saveComment();
}
@@ -635,12 +523,7 @@ export default function App({
</MainMenu.ItemCustom>
<MainMenu.DefaultItems.Help />
{excalidrawAPI && (
<MobileFooter
excalidrawLib={excalidrawLib}
excalidrawAPI={excalidrawAPI}
/>
)}
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
</MainMenu>
);
};
@@ -789,7 +672,83 @@ export default function App({
</div>
</div>
<div className="excalidraw-wrapper">
{renderExcalidraw(children)}
<Excalidraw
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
setExcalidrawAPI(api)
}
initialData={initialStatePromiseRef.current.promise}
onChange={(elements, state) => {
// console.info("Elements :", elements, "State : ", state);
}}
onPointerUpdate={(payload: {
pointer: { x: number; y: number };
button: "down" | "up";
pointersMap: Gesture["pointers"];
}) => setPointerData(payload)}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
theme={theme}
name="Custom name of drawing"
UIOptions={{
canvasActions: {
loadScene: false,
},
tools: { image: !disableImageTool },
}}
renderTopRightUI={renderTopRightUI}
onLinkOpen={onLinkOpen}
onPointerDown={onPointerDown}
onScrollChange={rerenderCommentIcons}
// allow all urls
validateEmbeddable={true}
>
{excalidrawAPI && (
<Footer>
<CustomFooter excalidrawAPI={excalidrawAPI} />
</Footer>
)}
<WelcomeScreen />
<Sidebar name="custom">
<Sidebar.Tabs>
<Sidebar.Header />
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
<Sidebar.TabTriggers>
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
</Sidebar.TabTriggers>
</Sidebar.Tabs>
</Sidebar>
<Sidebar.Trigger
name="custom"
tab="one"
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
bottom: "20px",
zIndex: 9999999999999999,
}}
>
Toggle Custom Sidebar
</Sidebar.Trigger>
{renderMenu()}
{excalidrawAPI && (
<TTDDialogTrigger icon={<span>😀</span>}>
Text to diagram
</TTDDialogTrigger>
)}
<TTDDialog
onTextSubmit={async (_) => {
console.info("submit");
// sleep for 2s
await new Promise((resolve) => setTimeout(resolve, 2000));
throw new Error("error, go away now");
// return "dummy";
}}
/>
</Excalidraw>
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
{comment && renderComment()}
</div>
@@ -1,6 +1,6 @@
import type * as TExcalidraw from "@excalidraw/excalidraw";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import type { ExcalidrawImperativeAPI } from "../types";
const { Button, MIME_TYPES } = window.ExcalidrawLib;
const COMMENT_SVG = (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -17,28 +17,24 @@ const COMMENT_SVG = (
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
);
const CustomFooter = ({
excalidrawAPI,
excalidrawLib,
}: {
excalidrawAPI: ExcalidrawImperativeAPI;
excalidrawLib: typeof TExcalidraw;
}) => {
const { Button, MIME_TYPES } = excalidrawLib;
return (
<>
<Button
onSelect={() => alert("General Kenobi!")}
style={{ marginLeft: "1rem", width: "auto" }}
className="you are a bold one"
style={{ marginLeft: "1rem" }}
title="Hello there!"
>
Hit me
{COMMENT_SVG}
</Button>
<Button
<button
className="custom-element"
onSelect={() => {
onClick={() => {
excalidrawAPI?.setActiveTool({
type: "custom",
customType: "comment",
@@ -61,10 +57,15 @@ const CustomFooter = ({
)}`;
excalidrawAPI?.setCursor(`url(${url}), auto`);
}}
title="Comments!"
>
{COMMENT_SVG}
</Button>
</button>
<button
className="custom-footer"
onClick={() => alert("This is dummy footer")}
>
custom footer
</button>
</>
);
};
@@ -0,0 +1,20 @@
import type { ExcalidrawImperativeAPI } from "../types";
import CustomFooter from "./CustomFooter";
const { useDevice, Footer } = window.ExcalidrawLib;
const MobileFooter = ({
excalidrawAPI,
}: {
excalidrawAPI: ExcalidrawImperativeAPI;
}) => {
const device = useDevice();
if (device.editor.isMobile) {
return (
<Footer>
<CustomFooter excalidrawAPI={excalidrawAPI} />
</Footer>
);
}
return null;
};
export default MobileFooter;
+17
View File
@@ -0,0 +1,17 @@
import App from "./App";
const { StrictMode } = window.React;
//@ts-ignore
const { createRoot } = window.ReactDOM;
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App
appTitle={"Excalidraw Example"}
useCustom={(api: any, args?: any[]) => {}}
/>
</StrictMode>,
);
@@ -1,5 +1,5 @@
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
import type { FileId } from "@excalidraw/excalidraw/element/types";
import type { ExcalidrawElementSkeleton } from "../data/transform";
import type { FileId } from "../element/types";
const elements: ExcalidrawElementSkeleton[] = [
{

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

@@ -13,20 +13,20 @@
window.name = "codesandbox";
</script>
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
<link rel="stylesheet" href="bundle.css" />
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
<!-- This is so that we use the bundled excalidraw.development.js file instead
of the actual source code -->
<script type="module">
import * as ExcalidrawLib from "@excalidraw/excalidraw";
console.log(ExcalidrawLib);
import * as ExcalidrawLib from "/dist/browser/dev/index.js";
window.ExcalidrawLib = ExcalidrawLib;
</script>
<script type="module" src="index.tsx"></script>
<script type="module" src="bundle.js"></script>
</body>
</html>
@@ -1,8 +1,9 @@
import { useState } from "react";
import "./ExampleSidebar.scss";
const React = window.React;
export default function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
const [open, setOpen] = React.useState(false);
return (
<>
+61 -166
View File
@@ -4,8 +4,6 @@ import {
isTextElement,
} from "./element";
import {
ElementsMap,
ElementsMapOrArray,
ExcalidrawElement,
ExcalidrawFrameLikeElement,
NonDeleted,
@@ -23,12 +21,8 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds";
import {
doLineSegmentsIntersect,
elementsOverlappingBBox,
} from "../utils/export";
import { doLineSegmentsIntersect } from "../utils/export";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import { ReadonlySetLike } from "./utility-types";
// --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = (
@@ -110,16 +104,17 @@ export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
) => {
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(frame);
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return (
frameX1 <= elementX1 &&
frameY1 <= elementY1 &&
frameX2 >= elementX2 &&
frameY2 >= elementY2
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2
);
};
@@ -214,17 +209,9 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => {
};
export const getFrameChildren = (
allElements: ElementsMapOrArray,
allElements: ExcalidrawElementsIncludingDeleted,
frameId: string,
) => {
const frameChildren: ExcalidrawElement[] = [];
for (const element of allElements.values()) {
if (element.frameId === frameId) {
frameChildren.push(element);
}
}
return frameChildren;
};
) => allElements.filter((element) => element.frameId === frameId);
export const getFrameLikeElements = (
allElements: ExcalidrawElementsIncludingDeleted,
@@ -382,107 +369,43 @@ export const getContainingFrame = (
// --------------------------- Frame Operations -------------------------------
/** */
export const filterElementsEligibleAsFrameChildren = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
) => {
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
elements = omitGroupsContainingFrameLikes(elements);
for (const element of elements) {
if (isFrameLikeElement(element) && element.id !== frame.id) {
otherFrames.add(element.id);
}
}
const processedGroups = new Set<ExcalidrawElement["id"]>();
const eligibleElements: ExcalidrawElement[] = [];
for (const element of elements) {
// don't add frames or their children
if (
isFrameLikeElement(element) ||
(element.frameId && otherFrames.has(element.frameId))
) {
continue;
}
if (element.groupIds.length) {
const shallowestGroupId = element.groupIds.at(-1)!;
if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId);
const groupElements = getElementsInGroup(elements, shallowestGroupId);
if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) {
for (const child of groupElements) {
eligibleElements.push(child);
}
}
}
} else {
const overlaps = elementOverlapsWithFrame(element, frame);
if (overlaps) {
eligibleElements.push(element);
}
}
}
return eligibleElements;
};
/**
* Retains (or repairs for target frame) the ordering invriant where children
* elements come right before the parent frame:
* [el, el, child, child, frame, el]
*
* @returns mutated allElements (same data structure)
*/
export const addElementsToFrame = <T extends ElementsMapOrArray>(
allElements: T,
export const addElementsToFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
): T => {
const elementsMap = arrayToMap(allElements);
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
for (const element of allElements.values()) {
if (element.frameId === frame.id) {
currTargetFrameChildrenMap.set(element.id, true);
}
}
) => {
const { currTargetFrameChildrenMap } = allElements.reduce(
(acc, element, index) => {
if (element.frameId === frame.id) {
acc.currTargetFrameChildrenMap.set(element.id, true);
}
return acc;
},
{
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
},
);
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
const finalElementsToAdd: ExcalidrawElement[] = [];
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
for (const element of elementsToAdd) {
if (isFrameLikeElement(element) && element.id !== frame.id) {
otherFrames.add(element.id);
}
}
// - add bound text elements if not already in the array
// - filter out elements that are already in the frame
for (const element of omitGroupsContainingFrameLikes(
allElements,
elementsToAdd,
)) {
// don't add frames or their children
if (
isFrameLikeElement(element) ||
(element.frameId && otherFrames.has(element.frameId))
) {
continue;
}
if (!currTargetFrameChildrenMap.has(element.id)) {
finalElementsToAdd.push(element);
}
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (
boundTextElement &&
!suppliedElementsToAddSet.has(boundTextElement.id) &&
@@ -501,13 +424,13 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
false,
);
}
return allElements;
return allElements.slice();
};
export const removeElementsFromFrame = (
elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
elementsMap: ElementsMap,
allElements: ExcalidrawElementsIncludingDeleted,
elementsToRemove: NonDeletedExcalidrawElement[],
appState: AppState,
) => {
const _elementsToRemove = new Map<
ExcalidrawElement["id"],
@@ -526,7 +449,7 @@ export const removeElementsFromFrame = (
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
arr.push(element);
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
_elementsToRemove.set(boundTextElement.id, boundTextElement);
arr.push(boundTextElement);
@@ -545,35 +468,35 @@ export const removeElementsFromFrame = (
false,
);
}
return allElements.slice();
};
export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
allElements: readonly T[],
export const removeAllElementsFromFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameLikeElement,
appState: AppState,
) => {
const elementsInFrame = getFrameChildren(allElements, frame.id);
removeElementsFromFrame(elementsInFrame, arrayToMap(allElements));
return allElements;
return removeElementsFromFrame(allElements, elementsInFrame, appState);
};
export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
allElements: readonly T[],
export const replaceAllElementsInFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
nextElementsInFrame: ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
app: AppClassProperties,
): T[] => {
appState: AppState,
) => {
return addElementsToFrame(
removeAllElementsFromFrame(allElements, frame),
removeAllElementsFromFrame(allElements, frame, appState),
nextElementsInFrame,
frame,
).slice();
);
};
/** does not mutate elements, but returns new ones */
export const updateFrameMembershipOfSelectedElements = <
T extends ElementsMapOrArray,
>(
allElements: T,
export const updateFrameMembershipOfSelectedElements = (
allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState,
app: AppClassProperties,
) => {
@@ -598,22 +521,19 @@ export const updateFrameMembershipOfSelectedElements = <
const elementsToRemove = new Set<ExcalidrawElement>();
const elementsMap = arrayToMap(allElements);
elementsToFilter.forEach((element) => {
if (
element.frameId &&
!isFrameLikeElement(element) &&
!isElementInFrame(element, elementsMap, appState)
!isElementInFrame(element, allElements, appState)
) {
elementsToRemove.add(element);
}
});
if (elementsToRemove.size > 0) {
removeElementsFromFrame(elementsToRemove, elementsMap);
}
return allElements;
return elementsToRemove.size > 0
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
: allElements;
};
/**
@@ -621,16 +541,14 @@ export const updateFrameMembershipOfSelectedElements = <
* anywhere in the group tree
*/
export const omitGroupsContainingFrameLikes = (
allElements: ElementsMapOrArray,
allElements: ExcalidrawElementsIncludingDeleted,
/** subset of elements you want to filter. Optional perf optimization so we
* don't have to filter all elements unnecessarily
*/
selectedElements?: readonly ExcalidrawElement[],
) => {
const uniqueGroupIds = new Set<string>();
const elements = selectedElements || allElements;
for (const el of elements.values()) {
for (const el of selectedElements || allElements) {
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
if (topMostGroupId) {
uniqueGroupIds.add(topMostGroupId);
@@ -648,15 +566,9 @@ export const omitGroupsContainingFrameLikes = (
}
}
const ret: ExcalidrawElement[] = [];
for (const element of elements.values()) {
if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) {
ret.push(element);
}
}
return ret;
return (selectedElements || allElements).filter(
(el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]),
);
};
/**
@@ -665,11 +577,10 @@ export const omitGroupsContainingFrameLikes = (
*/
export const getTargetFrame = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
appState: StaticCanvasAppState,
) => {
const _element = isTextElement(element)
? getContainerElement(element, elementsMap) || element
? getContainerElement(element) || element
: element;
return appState.selectedElementIds[_element.id] &&
@@ -682,12 +593,12 @@ export const getTargetFrame = (
// given an element, return if the element is in some frame
export const isElementInFrame = (
element: ExcalidrawElement,
allElements: ElementsMap,
allElements: ExcalidrawElementsIncludingDeleted,
appState: StaticCanvasAppState,
) => {
const frame = getTargetFrame(element, allElements, appState);
const frame = getTargetFrame(element, appState);
const _element = isTextElement(element)
? getContainerElement(element, allElements) || element
? getContainerElement(element) || element
: element;
if (frame) {
@@ -746,26 +657,10 @@ export const getFrameLikeTitle = (
element: ExcalidrawFrameLikeElement,
frameIdx: number,
) => {
const existingName = element.name?.trim();
if (existingName) {
return existingName;
}
// TODO name frames AI only is specific to AI frames
return element.name === null
? isFrameElement(element)
? `Frame ${frameIdx}`
: `AI Frame $${frameIdx}`
: element.name;
};
export const getElementsOverlappingFrame = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
) => {
return (
elementsOverlappingBBox({
elements,
bounds: frame,
type: "overlap",
})
// removes elements who are overlapping, but are in a different frame,
// and thus invisible in target frame
.filter((el) => !el.frameId || el.frameId === frame.id)
);
return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`;
};
+4 -14
View File
@@ -3,8 +3,6 @@ import {
ExcalidrawElement,
NonDeleted,
NonDeletedExcalidrawElement,
ElementsMapOrArray,
ElementsMap,
} from "./element/types";
import {
AppClassProperties,
@@ -272,17 +270,9 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
element.groupIds.includes(groupId);
export const getElementsInGroup = (
elements: ElementsMapOrArray,
elements: readonly ExcalidrawElement[],
groupId: string,
) => {
const elementsInGroup: ExcalidrawElement[] = [];
for (const element of elements.values()) {
if (isElementInGroup(element, groupId)) {
elementsInGroup.push(element);
}
}
return elementsInGroup;
};
) => elements.filter((element) => isElementInGroup(element, groupId));
export const getSelectedGroupIdForElement = (
element: ExcalidrawElement,
@@ -330,12 +320,12 @@ export const removeFromSelectedGroups = (
export const getMaximumGroups = (
elements: ExcalidrawElement[],
elementsMap: ElementsMap,
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
@@ -345,7 +335,7 @@ export const getMaximumGroups = (
const currentGroupMembers = groups.get(groupId) || [];
// Include bound text if present when grouping
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
currentGroupMembers.push(boundTextElement);
}
+1 -8
View File
@@ -80,13 +80,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
}
useEffect(() => {
const importPolyfill = async () => {
//@ts-ignore
await import("canvas-roundrect-polyfill");
};
importPolyfill();
// Block pinch-zooming on iOS outside of the content area
const handleTouchMove = (event: TouchEvent) => {
// @ts-ignore
@@ -230,7 +223,7 @@ export {
} from "../utils/export";
export { isLinearElement } from "./element/typeChecks";
export { FONT_FAMILY, THEME, MIME_TYPES, ROUNDNESS } from "./constants";
export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants";
export {
mutateElement,
+10 -31
View File
@@ -6,7 +6,6 @@ import {
ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
isTextElement,
@@ -22,11 +21,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
import type { Drawable } from "roughjs/bin/core";
import type { RoughSVG } from "roughjs/bin/svg";
import {
SVGRenderConfig,
StaticCanvasRenderConfig,
RenderableElementsMap,
} from "../scene/types";
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
import {
distance,
getFontString,
@@ -191,7 +186,6 @@ const cappedElementCanvasSize = (
const generateElementCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
zoom: Zoom,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
@@ -249,8 +243,7 @@ const generateElementCanvas = (
zoomValue: zoom.value,
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion:
getBoundTextElement(element, elementsMap)?.version || null,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
};
};
@@ -410,7 +403,6 @@ export const elementWithCanvasCache = new WeakMap<
const generateElementWithCanvas = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
) => {
@@ -420,9 +412,7 @@ const generateElementWithCanvas = (
prevElementWithCanvas &&
prevElementWithCanvas.zoomValue !== zoom.value &&
!appState?.shouldCacheIgnoreZoom;
const boundTextElementVersion =
getBoundTextElement(element, elementsMap)?.version || null;
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
if (
@@ -434,7 +424,6 @@ const generateElementWithCanvas = (
) {
const elementWithCanvas = generateElementCanvas(
element,
elementsMap,
zoom,
renderConfig,
appState,
@@ -452,7 +441,6 @@ const drawElementFromCanvas = (
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
allElementsMap: NonDeletedSceneElementsMap,
) => {
const element = elementWithCanvas.element;
const padding = getCanvasPadding(element);
@@ -472,8 +460,7 @@ const drawElementFromCanvas = (
context.save();
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
const boundTextElement = getBoundTextElement(element, allElementsMap);
const boundTextElement = getBoundTextElement(element);
if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas");
@@ -520,6 +507,7 @@ const drawElementFromCanvas = (
offsetY -
padding * zoom;
tempCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area
tempCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
@@ -581,7 +569,6 @@ const drawElementFromCanvas = (
) {
const textElement = getBoundTextElement(
element,
allElementsMap,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a";
@@ -589,7 +576,7 @@ const drawElementFromCanvas = (
context.strokeRect(
(coords.x + appState.scrollX) * window.devicePixelRatio,
(coords.y + appState.scrollY) * window.devicePixelRatio,
getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio,
getBoundTextMaxWidth(element) * window.devicePixelRatio,
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
);
}
@@ -624,8 +611,6 @@ export const renderSelectionElement = (
export const renderElement = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
allElementsMap: NonDeletedSceneElementsMap,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,
@@ -697,7 +682,6 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
renderConfig,
appState,
);
@@ -706,7 +690,6 @@ export const renderElement = (
context,
renderConfig,
appState,
allElementsMap,
);
}
@@ -732,7 +715,7 @@ export const renderElement = (
let shiftX = (x2 - x1) / 2 - (element.x - x1);
let shiftY = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) {
const container = getContainerElement(element, elementsMap);
const container = getContainerElement(element);
if (isArrowElement(container)) {
const boundTextCoords =
LinearElementEditor.getBoundTextElementPosition(
@@ -749,7 +732,7 @@ export const renderElement = (
if (shouldResetImageFilter(element, renderConfig, appState)) {
context.filter = "none";
}
const boundTextElement = getBoundTextElement(element, elementsMap);
const boundTextElement = getBoundTextElement(element);
if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas");
@@ -832,7 +815,6 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas(
element,
elementsMap,
renderConfig,
appState,
);
@@ -864,7 +846,6 @@ export const renderElement = (
context,
renderConfig,
appState,
allElementsMap,
);
// reset
@@ -919,7 +900,6 @@ const maybeWrapNodesInFrameClipPath = (
export const renderElementToSvg = (
element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap,
rsvg: RoughSVG,
svgRoot: SVGElement,
files: BinaryFiles,
@@ -932,7 +912,7 @@ export const renderElementToSvg = (
let cx = (x2 - x1) / 2 - (element.x - x1);
let cy = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) {
const container = getContainerElement(element, elementsMap);
const container = getContainerElement(element);
if (isArrowElement(container)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
@@ -1033,7 +1013,6 @@ export const renderElementToSvg = (
createPlaceholderEmbeddableLabel(element);
renderElementToSvg(
label,
elementsMap,
rsvg,
root,
files,
@@ -1110,7 +1089,7 @@ export const renderElementToSvg = (
}
case "line":
case "arrow": {
const boundText = getBoundTextElement(element, elementsMap);
const boundText = getBoundTextElement(element);
const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
if (boundText) {
maskPath.setAttribute("id", `mask-${element.id}`);
+88 -119
View File
@@ -33,7 +33,6 @@ import {
SVGRenderConfig,
StaticCanvasRenderConfig,
StaticSceneRenderConfig,
RenderableElementsMap,
} from "../scene/types";
import {
getScrollBars,
@@ -62,7 +61,7 @@ import {
TransformHandles,
TransformHandleType,
} from "../element/transformHandles";
import { arrayToMap, throttleRAF } from "../utils";
import { throttleRAF } from "../utils";
import { UserIdleState } from "../types";
import { FRAME_STYLE, THEME_FILTER } from "../constants";
import {
@@ -76,12 +75,16 @@ import {
isIframeLikeElement,
isLinearElement,
} from "../element/typeChecks";
import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
import {
isIframeLikeOrItsLabel,
createPlaceholderEmbeddableLabel,
} from "../element/embeddable";
import {
elementOverlapsWithFrame,
getTargetFrame,
isElementInFrame,
} from "../frame";
import "canvas-roundrect-polyfill";
export const DEFAULT_SPACING = 2;
@@ -246,7 +249,6 @@ const renderLinearPointHandles = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: RenderableElementsMap,
) => {
if (!appState.selectedLinearElement) {
return;
@@ -270,7 +272,6 @@ const renderLinearPointHandles = (
//Rendering segment mid points
const midPoints = LinearElementEditor.getEditorMidPoints(
element,
elementsMap,
appState,
).filter((midPoint) => midPoint !== null) as Point[];
@@ -445,7 +446,7 @@ const bootstrapCanvas = ({
const _renderInteractiveScene = ({
canvas,
elementsMap,
elements,
visibleElements,
selectedElements,
scale,
@@ -453,7 +454,7 @@ const _renderInteractiveScene = ({
renderConfig,
}: InteractiveSceneRenderConfig) => {
if (canvas === null) {
return { atLeastOneVisibleElement: false, elementsMap };
return { atLeastOneVisibleElement: false, elements };
}
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
@@ -487,12 +488,7 @@ const _renderInteractiveScene = ({
});
if (editingLinearElement) {
renderLinearPointHandles(
context,
appState,
editingLinearElement,
elementsMap,
);
renderLinearPointHandles(context, appState, editingLinearElement);
}
// Paint selection element
@@ -535,7 +531,6 @@ const _renderInteractiveScene = ({
context,
appState,
selectedElements[0] as NonDeleted<ExcalidrawLinearElement>,
elementsMap,
);
}
@@ -561,71 +556,81 @@ const _renderInteractiveScene = ({
context,
appState,
selectedElements[0] as ExcalidrawLinearElement,
elementsMap,
);
}
const selectionColor = renderConfig.selectionColor || oc.black;
if (showBoundingBox) {
// Optimisation for finding quickly relevant element ids
const locallySelectedIds = arrayToMap(selectedElements);
const locallySelectedIds = selectedElements.reduce(
(acc: Record<string, boolean>, element) => {
acc[element.id] = true;
return acc;
},
{},
);
const selections: {
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[] = [];
const selections = elements.reduce(
(
acc: {
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[],
element,
) => {
const selectionColors = [];
// local user
if (
locallySelectedIds[element.id] &&
!isSelectedViaGroup(appState, element)
) {
selectionColors.push(selectionColor);
}
// remote users
if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(socketId: string) => {
const background = getClientColor(socketId);
return background;
},
),
);
}
for (const element of elementsMap.values()) {
const selectionColors = [];
// local user
if (
locallySelectedIds.has(element.id) &&
!isSelectedViaGroup(appState, element)
) {
selectionColors.push(selectionColor);
}
// remote users
if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(socketId: string) => {
const background = getClientColor(socketId);
return background;
},
),
);
}
if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true);
selections.push({
angle: element.angle,
elementX1,
elementY1,
elementX2,
elementY2,
selectionColors,
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
cx,
cy,
activeEmbeddable:
appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active",
});
}
}
if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true);
acc.push({
angle: element.angle,
elementX1,
elementY1,
elementX2,
elementY2,
selectionColors,
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
cx,
cy,
activeEmbeddable:
appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active",
});
}
return acc;
},
[],
);
const addSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elementsMap, groupId);
const groupElements = getElementsInGroup(elements, groupId);
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(groupElements);
selections.push({
@@ -865,7 +870,7 @@ const _renderInteractiveScene = ({
let scrollBars;
if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars(
elementsMap,
elements,
normalizedWidth,
normalizedHeight,
appState,
@@ -892,15 +897,14 @@ const _renderInteractiveScene = ({
return {
scrollBars,
atLeastOneVisibleElement: visibleElements.length > 0,
elementsMap,
elements,
};
};
const _renderStaticScene = ({
canvas,
rc,
elementsMap,
allElementsMap,
elements,
visibleElements,
scale,
appState,
@@ -961,7 +965,7 @@ const _renderStaticScene = ({
// Paint visible elements
visibleElements
.filter((el) => !isIframeLikeElement(el))
.filter((el) => !isIframeLikeOrItsLabel(el))
.forEach((element) => {
try {
const frameId = element.frameId || appState.frameToHighlight?.id;
@@ -973,32 +977,16 @@ const _renderStaticScene = ({
) {
context.save();
const frame = getTargetFrame(element, elementsMap, appState);
const frame = getTargetFrame(element, appState);
// TODO do we need to check isElementInFrame here?
if (frame && isElementInFrame(element, elementsMap, appState)) {
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig, appState);
}
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
renderElement(element, rc, context, renderConfig, appState);
context.restore();
} else {
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
renderElement(element, rc, context, renderConfig, appState);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
@@ -1010,19 +998,11 @@ const _renderStaticScene = ({
// render embeddables on top
visibleElements
.filter((el) => isIframeLikeElement(el))
.filter((el) => isIframeLikeOrItsLabel(el))
.forEach((element) => {
try {
const render = () => {
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
renderElement(element, rc, context, renderConfig, appState);
if (
isIframeLikeElement(element) &&
@@ -1034,15 +1014,7 @@ const _renderStaticScene = ({
element.height
) {
const label = createPlaceholderEmbeddableLabel(element);
renderElement(
label,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
renderElement(label, rc, context, renderConfig, appState);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
@@ -1060,9 +1032,9 @@ const _renderStaticScene = ({
) {
context.save();
const frame = getTargetFrame(element, elementsMap, appState);
const frame = getTargetFrame(element, appState);
if (frame && isElementInFrame(element, elementsMap, appState)) {
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig, appState);
}
render();
@@ -1476,7 +1448,6 @@ const renderLinkIcon = (
// This should be only called for exporting purposes
export const renderSceneToSvg = (
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: RenderableElementsMap,
rsvg: RoughSVG,
svgRoot: SVGElement,
files: BinaryFiles,
@@ -1488,13 +1459,12 @@ export const renderSceneToSvg = (
// render elements
elements
.filter((el) => !isIframeLikeElement(el))
.filter((el) => !isIframeLikeOrItsLabel(el))
.forEach((element) => {
if (!element.isDeleted) {
try {
renderElementToSvg(
element,
elementsMap,
rsvg,
svgRoot,
files,
@@ -1516,7 +1486,6 @@ export const renderSceneToSvg = (
try {
renderElementToSvg(
element,
elementsMap,
rsvg,
svgRoot,
files,
+1 -8
View File
@@ -1,6 +1,5 @@
import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement";
import { getContainerElement } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { getFontString } from "../utils";
@@ -58,13 +57,7 @@ export class Fonts {
ShapeCache.delete(element);
didUpdate = true;
return newElementWith(element, {
...refreshTextDimensions(
element,
getContainerElement(
element,
this.scene.getElementsMapIncludingDeleted(),
),
),
...refreshTextDimensions(element),
});
}
return element;
+23 -39
View File
@@ -1,14 +1,10 @@
import { isElementInViewport } from "../element/sizeHelpers";
import { isImageElement } from "../element/typeChecks";
import {
NonDeletedElementsMap,
NonDeletedExcalidrawElement,
} from "../element/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { cancelRender } from "../renderer/renderScene";
import { AppState } from "../types";
import { memoize, toBrandedType } from "../utils";
import { memoize } from "../utils";
import Scene from "./Scene";
import { RenderableElementsMap } from "./types";
export class Renderer {
private scene: Scene;
@@ -19,7 +15,7 @@ export class Renderer {
public getRenderableElements = (() => {
const getVisibleCanvasElements = ({
elementsMap,
elements,
zoom,
offsetLeft,
offsetTop,
@@ -28,7 +24,7 @@ export class Renderer {
height,
width,
}: {
elementsMap: NonDeletedElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
zoom: AppState["zoom"];
offsetLeft: AppState["offsetLeft"];
offsetTop: AppState["offsetTop"];
@@ -37,55 +33,43 @@ export class Renderer {
height: AppState["height"];
width: AppState["width"];
}): readonly NonDeletedExcalidrawElement[] => {
const visibleElements: NonDeletedExcalidrawElement[] = [];
for (const element of elementsMap.values()) {
if (
isElementInViewport(element, width, height, {
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY,
})
) {
visibleElements.push(element);
}
}
return visibleElements;
return elements.filter((element) =>
isElementInViewport(element, width, height, {
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY,
}),
);
};
const getRenderableElements = ({
elements,
const getCanvasElements = ({
editingElement,
elements,
pendingImageElementId,
}: {
elements: readonly NonDeletedExcalidrawElement[];
editingElement: AppState["editingElement"];
pendingImageElementId: AppState["pendingImageElementId"];
}) => {
const elementsMap = toBrandedType<RenderableElementsMap>(new Map());
for (const element of elements) {
return elements.filter((element) => {
if (isImageElement(element)) {
if (
// => not placed on canvas yet (but in elements array)
pendingImageElementId === element.id
) {
continue;
return false;
}
}
// we don't want to render text element that's being currently edited
// (it's rendered on remote only)
if (
return (
!editingElement ||
editingElement.type !== "text" ||
element.id !== editingElement.id
) {
elementsMap.set(element.id, element);
}
}
return elementsMap;
);
});
};
return memoize(
@@ -116,14 +100,14 @@ export class Renderer {
}) => {
const elements = this.scene.getNonDeletedElements();
const elementsMap = getRenderableElements({
const canvasElements = getCanvasElements({
elements,
editingElement,
pendingImageElementId,
});
const visibleElements = getVisibleCanvasElements({
elementsMap,
elements: canvasElements,
zoom,
offsetLeft,
offsetTop,
@@ -133,7 +117,7 @@ export class Renderer {
width,
});
return { elementsMap, visibleElements };
return { canvasElements, visibleElements };
},
);
})();
+12 -61
View File
@@ -3,18 +3,14 @@ import {
NonDeletedExcalidrawElement,
NonDeleted,
ExcalidrawFrameLikeElement,
ElementsMapOrArray,
SceneElementsMap,
NonDeletedSceneElementsMap,
} from "../element/types";
import { isNonDeletedElement } from "../element";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isFrameLikeElement } from "../element/typeChecks";
import { getSelectedElements } from "./selection";
import { AppState } from "../types";
import { Assert, SameType } from "../utility-types";
import { randomInteger } from "../random";
import { toBrandedType } from "../utils";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
@@ -24,20 +20,6 @@ type SceneStateCallbackRemover = () => void;
type SelectionHash = string & { __brand: "selectionHash" };
const getNonDeletedElements = <T extends ExcalidrawElement>(
allElements: readonly T[],
) => {
const elementsMap = new Map() as NonDeletedSceneElementsMap;
const elements: T[] = [];
for (const element of allElements) {
if (!element.isDeleted) {
elements.push(element as NonDeleted<T>);
elementsMap.set(element.id, element as NonDeletedExcalidrawElement);
}
}
return { elementsMap, elements };
};
const hashSelectionOpts = (
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
) => {
@@ -120,14 +102,11 @@ class Scene {
private callbacks: Set<SceneStateCallback> = new Set();
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private nonDeletedElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
new Map(),
);
private elements: readonly ExcalidrawElement[] = [];
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
[];
private frames: readonly ExcalidrawFrameLikeElement[] = [];
private elementsMap = toBrandedType<SceneElementsMap>(new Map());
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
private selectedElementsCache: {
selectedElementIds: AppState["selectedElementIds"] | null;
elements: readonly NonDeletedExcalidrawElement[] | null;
@@ -139,14 +118,6 @@ class Scene {
};
private versionNonce: number | undefined;
getElementsMapIncludingDeleted() {
return this.elementsMap;
}
getNonDeletedElementsMap() {
return this.nonDeletedElementsMap;
}
getElementsIncludingDeleted() {
return this.elements;
}
@@ -167,7 +138,7 @@ class Scene {
* scene state. This in effect will likely result in cache-miss, and
* the cache won't be updated in this case.
*/
elements?: ElementsMapOrArray;
elements?: readonly ExcalidrawElement[];
// selection-related options
includeBoundTextElement?: boolean;
includeElementsInFrames?: boolean;
@@ -256,27 +227,23 @@ class Scene {
return didChange;
}
replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) {
this.elements =
// ts doesn't like `Array.isArray` of `instanceof Map`
nextElements instanceof Array
? nextElements
: Array.from(nextElements.values());
replaceAllElements(
nextElements: readonly ExcalidrawElement[],
mapElementIds = true,
) {
this.elements = nextElements;
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
this.elementsMap.clear();
this.elements.forEach((element) => {
nextElements.forEach((element) => {
if (isFrameLikeElement(element)) {
nextFrameLikes.push(element);
}
this.elementsMap.set(element.id, element);
Scene.mapElementToScene(element, this, mapElementIds);
Scene.mapElementToScene(element, this);
});
const nonDeletedElements = getNonDeletedElements(this.elements);
this.nonDeletedElements = nonDeletedElements.elements;
this.nonDeletedElementsMap = nonDeletedElements.elementsMap;
this.nonDeletedElements = getNonDeletedElements(this.elements);
this.frames = nextFrameLikes;
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames);
this.informMutation();
}
@@ -365,22 +332,6 @@ class Scene {
getElementIndex(elementId: string) {
return this.elements.findIndex((element) => element.id === elementId);
}
getContainerElement = (
element:
| (ExcalidrawElement & {
containerId: ExcalidrawElement["id"] | null;
})
| null,
) => {
if (!element) {
return null;
}
if (element.containerId) {
return this.getElement(element.containerId) || null;
}
return null;
};
}
export default Scene;
+24 -40
View File
@@ -4,7 +4,6 @@ import {
ExcalidrawFrameLikeElement,
ExcalidrawTextElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
Bounds,
@@ -12,13 +11,7 @@ import {
getElementAbsoluteCoords,
} from "../element/bounds";
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
import {
arrayToMap,
cloneJSON,
distance,
getFontString,
toBrandedType,
} from "../utils";
import { cloneJSON, distance, getFontString } from "../utils";
import { AppState, BinaryFiles } from "../types";
import {
DEFAULT_EXPORT_PADDING,
@@ -33,8 +26,8 @@ import {
getInitializedImageElements,
updateImageCache,
} from "../element/image";
import { elementsOverlappingBBox } from "../../utils/export";
import {
getElementsOverlappingFrame,
getFrameLikeElements,
getFrameLikeTitle,
getRootElements,
@@ -44,7 +37,6 @@ import { Mutable } from "../utility-types";
import { newElementWith } from "../element/mutateElement";
import Scene from "./Scene";
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
import { RenderableElementsMap } from "./types";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@@ -176,7 +168,11 @@ const prepareElementsForRender = ({
let nextElements: readonly ExcalidrawElement[];
if (exportingFrame) {
nextElements = getElementsOverlappingFrame(elements, exportingFrame);
nextElements = elementsOverlappingBBox({
elements,
bounds: exportingFrame,
type: "overlap",
});
} else if (frameRendering.enabled && frameRendering.name) {
nextElements = addFrameLabelsAsTextElements(elements, {
exportWithDarkMode,
@@ -252,12 +248,7 @@ export const exportToCanvas = async (
renderStaticScene({
canvas,
rc: rough.canvas(canvas),
elementsMap: toBrandedType<RenderableElementsMap>(
arrayToMap(elementsForRender),
),
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
arrayToMap(elements),
),
elements: elementsForRender,
visibleElements: elementsForRender,
scale,
appState: {
@@ -445,29 +436,22 @@ export const exportToSvg = async (
const renderEmbeddables = opts?.renderEmbeddables ?? false;
renderSceneToSvg(
elementsForRender,
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
rsvg,
svgRoot,
files || {},
{
offsetX,
offsetY,
isExporting: true,
exportWithDarkMode,
renderEmbeddables,
frameRendering,
canvasBackgroundColor: viewBackgroundColor,
embedsValidationStatus: renderEmbeddables
? new Map(
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
: new Map(),
},
);
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
offsetX,
offsetY,
isExporting: true,
exportWithDarkMode,
renderEmbeddables,
frameRendering,
canvasBackgroundColor: viewBackgroundColor,
embedsValidationStatus: renderEmbeddables
? new Map(
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
: new Map(),
});
tempScene.destroy();
+4 -3
View File
@@ -1,6 +1,7 @@
import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element";
import { InteractiveCanvasAppState } from "../types";
import { RenderableElementsMap, ScrollBars } from "./types";
import { ScrollBars } from "./types";
import { getGlobalCSSVariable } from "../utils";
import { getLanguage } from "../i18n";
@@ -9,12 +10,12 @@ export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
export const getScrollBars = (
elements: RenderableElementsMap,
elements: readonly ExcalidrawElement[],
viewportWidth: number,
viewportHeight: number,
appState: InteractiveCanvasAppState,
): ScrollBars => {
if (!elements.size) {
if (elements.length === 0) {
return {
horizontal: null,
vertical: null,
+12 -12
View File
@@ -1,5 +1,4 @@
import {
ElementsMapOrArray,
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
@@ -12,6 +11,7 @@ import {
getFrameChildren,
} from "../frame";
import { isShallowEqual } from "../utils";
import { arrayToMap } from "../utils";
import { isElementInViewport } from "../element/sizeHelpers";
/**
@@ -49,11 +49,13 @@ export const getElementsWithinSelection = (
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(selection);
const elementsMap = arrayToMap(elements);
let elementsInSelection = elements.filter((element) => {
let [elementX1, elementY1, elementX2, elementY2] =
getElementBounds(element);
const containingFrame = getContainingFrame(element);
const containingFrame = getContainingFrame(element, elementsMap);
if (containingFrame) {
const [fx1, fy1, fx2, fy2] = getElementBounds(containingFrame);
@@ -79,7 +81,7 @@ export const getElementsWithinSelection = (
: elementsInSelection;
elementsInSelection = elementsInSelection.filter((element) => {
const containingFrame = getContainingFrame(element);
const containingFrame = getContainingFrame(element, elementsMap);
if (containingFrame) {
return elementOverlapsWithFrame(element, containingFrame);
@@ -167,28 +169,26 @@ export const getCommonAttributeOfSelectedElements = <T>(
};
export const getSelectedElements = (
elements: ElementsMapOrArray,
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
opts?: {
includeBoundTextElement?: boolean;
includeElementsInFrames?: boolean;
},
) => {
const selectedElements: ExcalidrawElement[] = [];
for (const element of elements.values()) {
const selectedElements = elements.filter((element) => {
if (appState.selectedElementIds[element.id]) {
selectedElements.push(element);
continue;
return element;
}
if (
opts?.includeBoundTextElement &&
isBoundToContainer(element) &&
appState.selectedElementIds[element?.containerId]
) {
selectedElements.push(element);
continue;
return element;
}
}
return null;
});
if (opts?.includeElementsInFrames) {
const elementsToInclude: ExcalidrawElement[] = [];
@@ -208,7 +208,7 @@ export const getSelectedElements = (
};
export const getTargetElements = (
elements: ElementsMapOrArray,
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
) =>
appState.editingElement
+3 -10
View File
@@ -2,9 +2,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core";
import {
ExcalidrawTextElement,
NonDeletedElementsMap,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import {
AppClassProperties,
@@ -14,10 +12,6 @@ import {
InteractiveCanvasAppState,
StaticCanvasAppState,
} from "../types";
import { MakeBrand } from "../utility-types";
export type RenderableElementsMap = NonDeletedElementsMap &
MakeBrand<"RenderableElementsMap">;
export type StaticCanvasRenderConfig = {
canvasBackgroundColor: AppState["viewBackgroundColor"];
@@ -59,15 +53,14 @@ export type InteractiveCanvasRenderConfig = {
export type RenderInteractiveSceneCallback = {
atLeastOneVisibleElement: boolean;
elementsMap: RenderableElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
scrollBars?: ScrollBars;
};
export type StaticSceneRenderConfig = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elementsMap: RenderableElementsMap;
allElementsMap: NonDeletedSceneElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
scale: number;
appState: StaticCanvasAppState;
@@ -76,7 +69,7 @@ export type StaticSceneRenderConfig = {
export type InteractiveSceneRenderConfig = {
canvas: HTMLCanvasElement | null;
elementsMap: RenderableElementsMap;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
selectedElements: readonly NonDeletedExcalidrawElement[];
scale: number;
+2 -6
View File
@@ -16,7 +16,6 @@ import { KEYS } from "./keys";
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
import { getVisibleAndNonSelectedElements } from "./scene/selection";
import { AppState, KeyboardModifiersObject, Point } from "./types";
import { arrayToMap } from "./utils";
const SNAP_DISTANCE = 8;
@@ -287,10 +286,7 @@ export const getVisibleGaps = (
appState,
);
const referenceBounds = getMaximumGroups(
referenceElements,
arrayToMap(elements),
)
const referenceBounds = getMaximumGroups(referenceElements)
.filter(
(elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@@ -576,7 +572,7 @@ export const getReferenceSnapPoints = (
appState,
);
return getMaximumGroups(referenceElements, arrayToMap(elements))
return getMaximumGroups(referenceElements)
.filter(
(elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@@ -263,170 +263,3 @@ describe("Paste bound text container", () => {
});
});
});
describe("pasting & frames", () => {
it("should add pasted elements to frame under cursor", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({ type: "rectangle" });
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect],
files: null,
});
mouse.moveTo(50, 50);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(2);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
});
});
it("should filter out elements not overlapping frame", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 100,
y: 100,
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(3);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(null);
});
});
it("should not filter out elements not overlapping frame if part of group", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
groupIds: ["g1"],
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 100,
y: 100,
groupIds: ["g1"],
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(3);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(frame.id);
});
});
it("should not filter out other frames and their children", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
groupIds: ["g1"],
});
const frame2 = API.createElement({
type: "frame",
width: 75,
height: 75,
x: 0,
y: 0,
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 55,
y: 55,
frameId: frame2.id,
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2, frame2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(4);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(h.elements[3].id);
expect(h.elements[3].type).toBe(frame2.type);
expect(h.elements[3].frameId).toBe(null);
});
});
});
-2
View File
@@ -206,8 +206,6 @@ export class Pointer {
moveTo(x: number = this.clientX, y: number = this.clientY) {
this.clientX = x;
this.clientY = y;
// fire "mousemove" to update editor cursor position
fireEvent.mouseMove(document, this.getEvent());
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
}

Some files were not shown because too many files have changed in this diff Show More