mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-05 16:47:03 +02:00
Compare commits
63 Commits
zsviczian-
...
mrazator/d
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d2038b7c5a | ||
![]() |
1913599594 | ||
![]() |
debf2ad608 | ||
![]() |
8fb2f70414 | ||
![]() |
5fc13e4309 | ||
![]() |
b5d60973b7 | ||
![]() |
a5d6939826 | ||
![]() |
0cf36d6b30 | ||
![]() |
58f7d33d80 | ||
![]() |
6fe7de8020 | ||
![]() |
01304aac49 | ||
![]() |
dff69e9191 | ||
![]() |
6fc85022ae | ||
![]() |
e48b63a0ae | ||
![]() |
c2caf78e95 | ||
![]() |
ce267aa0d3 | ||
![]() |
6e47fadb59 | ||
![]() |
b3d5ba0567 | ||
![]() |
c79e892e55 | ||
![]() |
57a9e301d4 | ||
![]() |
7c58477382 | ||
![]() |
83fac6d0db | ||
![]() |
f2e8404c7b | ||
![]() |
d797c2e210 | ||
![]() |
de81ba25fd | ||
![]() |
0cd5a259ae | ||
![]() |
432a46ef9e | ||
![]() |
a18f059188 | ||
![]() |
ab89d4c16f | ||
![]() |
6c3a434f2a | ||
![]() |
e1bb59fb8f | ||
![]() |
77aca48c84 | ||
![]() |
58990b41ae | ||
![]() |
99d8bff175 | ||
![]() |
30983d801a | ||
![]() |
21ffaf4d76 | ||
![]() |
82b9a6b464 | ||
![]() |
817d8c553c | ||
![]() |
69bc5bdaab | ||
![]() |
d587b8a3de | ||
![]() |
858c65b314 | ||
![]() |
f00069be68 | ||
![]() |
7b72406824 | ||
![]() |
49925038fd | ||
![]() |
05ba0339fe | ||
![]() |
cdd7f6158b | ||
![]() |
7e0f5b6369 | ||
![]() |
310a9ae4e0 | ||
![]() |
c57249481e | ||
![]() |
e72d83541a | ||
![]() |
9f8c87ae8c | ||
![]() |
f6061f5ec6 | ||
![]() |
12be5d716b | ||
![]() |
1abb901ec2 | ||
![]() |
6a17541713 | ||
![]() |
040a57f56a | ||
![]() |
15d2942aaa | ||
![]() |
59a0653fd4 | ||
![]() |
725c25c966 | ||
![]() |
d2fed34a30 | ||
![]() |
f12ed8e0b2 | ||
![]() |
508cfbc843 | ||
![]() |
245d681b7d |
@@ -48,3 +48,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
|||||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||||
HQIDAQAB'
|
HQIDAQAB'
|
||||||
|
|
||||||
|
# set to true in .env.development.local to disable the prevent unload dialog
|
||||||
|
VITE_APP_DISABLE_PREVENT_UNLOAD=
|
||||||
|
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"import/order": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "@excalidraw/**",
|
||||||
|
"group": "external",
|
||||||
|
"position": "after"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"newlines-between": "always-and-inside-groups",
|
||||||
|
"warnOnUnassignedImports": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
"no-restricted-globals": "off",
|
"no-restricted-globals": "off",
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||||
|
|
||||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
This will only work for `Desktop` devices.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
All `props` are _optional_.
|
All `props` are _optional_.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||||
@@ -13,7 +13,7 @@ All `props` are _optional_.
|
|||||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||||
@@ -29,8 +29,9 @@ All `props` are _optional_.
|
|||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
|||||||
|
|
||||||
[http://localhost:3001](http://localhost:3001) will open in your default browser.
|
[http://localhost:3001](http://localhost:3001) will open in your default browser.
|
||||||
|
|
||||||
This is the same example as the [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/mrazator/release-v18/examples/with-script-in-browser) example.
|
This is the same example as the [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example.
|
||||||
|
|
||||||
## Releasing
|
## Releasing
|
||||||
|
|
||||||
|
@@ -52,4 +52,4 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
Go to [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/mrazator/release-v18/examples/with-script-in-browser) example.
|
Go to [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example.
|
||||||
|
@@ -131,7 +131,7 @@ If you are using `pages router` then importing the wrapper dynamically would wor
|
|||||||
{/* Link should be updated to point to the latest! */}
|
{/* Link should be updated to point to the latest! */}
|
||||||
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs.vercel.app/).
|
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs.vercel.app/).
|
||||||
|
|
||||||
The `types` are available at `@excalidraw/excalidraw/types`, check [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/mrazator/release-v18/examples/with-script-in-browser) example for details.
|
The `types` are available at `@excalidraw/excalidraw/types`, check [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example for details.
|
||||||
|
|
||||||
### Preact
|
### Preact
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ import TabItem from "@theme/TabItem";
|
|||||||
|
|
||||||
```js showLineNumbers
|
```js showLineNumbers
|
||||||
// See https://www.npmjs.com/package/@excalidraw/excalidraw documentation.
|
// See https://www.npmjs.com/package/@excalidraw/excalidraw documentation.
|
||||||
import * as ExcalidrawLib from 'https://esm.sh/@excalidraw/excalidraw@0.18.0-rc.1/dist/dev/index.js?external=react,react-dom';
|
import * as ExcalidrawLib from 'https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom';
|
||||||
import React from "https://esm.sh/react@19.0.0";
|
import React from "https://esm.sh/react@19.0.0";
|
||||||
import ReactDOM from "https://esm.sh/react-dom@19.0.0"
|
import ReactDOM from "https://esm.sh/react-dom@19.0.0"
|
||||||
|
|
||||||
@@ -235,4 +235,4 @@ root.render(React.createElement(App));
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
You can try it out [here](https://jsfiddle.net/64y130L8/1/).
|
You can try it out [here](https://jsfiddle.net/vfn6dm14/3/).
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"@docusaurus/core": "2.2.0",
|
"@docusaurus/core": "2.2.0",
|
||||||
"@docusaurus/preset-classic": "2.2.0",
|
"@docusaurus/preset-classic": "2.2.0",
|
||||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||||
"@excalidraw/excalidraw": "0.18.0-rc.5",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"docusaurus-plugin-sass": "0.2.3",
|
"docusaurus-plugin-sass": "0.2.3",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
const FeatureList = [
|
const FeatureList = [
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
type FeatureItem = {
|
type FeatureItem = {
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Layout from "@theme/Layout";
|
|
||||||
import Link from "@docusaurus/Link";
|
import Link from "@docusaurus/Link";
|
||||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||||
import styles from "./index.module.css";
|
|
||||||
import HomepageFeatures from "@site/src/components/Homepage";
|
import HomepageFeatures from "@site/src/components/Homepage";
|
||||||
|
import Layout from "@theme/Layout";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
const { siteConfig } = useDocusaurusContext();
|
const { siteConfig } = useDocusaurusContext();
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
// Import the original mapper
|
// Import the original mapper
|
||||||
import MDXComponents from "@theme-original/MDXComponents";
|
|
||||||
import Highlight from "@site/src/components/Highlight";
|
import Highlight from "@site/src/components/Highlight";
|
||||||
|
import MDXComponents from "@theme-original/MDXComponents";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Re-use the default mapping
|
// Re-use the default mapping
|
||||||
|
@@ -12,7 +12,7 @@ if (ExecutionEnvironment.canUseDOM) {
|
|||||||
const Excalidraw = React.forwardRef((props, ref) => {
|
const Excalidraw = React.forwardRef((props, ref) => {
|
||||||
if (!window.EXCALIDRAW_ASSET_PATH) {
|
if (!window.EXCALIDRAW_ASSET_PATH) {
|
||||||
window.EXCALIDRAW_ASSET_PATH =
|
window.EXCALIDRAW_ASSET_PATH =
|
||||||
"https://esm.sh/@excalidraw/excalidraw@0.18.0-rc.5/dist/prod/";
|
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
@@ -1735,16 +1735,16 @@
|
|||||||
url-loader "^4.1.1"
|
url-loader "^4.1.1"
|
||||||
webpack "^5.73.0"
|
webpack "^5.73.0"
|
||||||
|
|
||||||
"@excalidraw/excalidraw@0.18.0-rc.5":
|
"@excalidraw/excalidraw@0.18.0":
|
||||||
version "0.18.0-rc.5"
|
version "0.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.18.0-rc.5.tgz#c55598e01808693702251322e59bf9dba931b6e0"
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.18.0.tgz#9f818e2df80a8735af54f8cc21da67997785532f"
|
||||||
integrity sha512-f6Z6cWlx+FWl1nxCu5F6OdKm9ooV/FPjndjIfFhGLVyERKATXhuNwZUHWwqsEW+SIGmiPG2515NG9KIOhjGd5g==
|
integrity sha512-QkIiS+5qdy8lmDWTKsuy0sK/fen/LRDtbhm2lc2xcFcqhv2/zdg95bYnl+wnwwXGHo7kEmP65BSiMHE7PJ3Zpw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@braintree/sanitize-url" "6.0.2"
|
"@braintree/sanitize-url" "6.0.2"
|
||||||
"@excalidraw/laser-pointer" "1.3.1"
|
"@excalidraw/laser-pointer" "1.3.1"
|
||||||
"@excalidraw/mermaid-to-excalidraw" "1.1.2"
|
"@excalidraw/mermaid-to-excalidraw" "1.1.2"
|
||||||
"@excalidraw/random-username" "1.1.0"
|
"@excalidraw/random-username" "1.1.0"
|
||||||
"@radix-ui/react-popover" "1.0.3"
|
"@radix-ui/react-popover" "1.1.6"
|
||||||
"@radix-ui/react-tabs" "1.0.2"
|
"@radix-ui/react-tabs" "1.0.2"
|
||||||
browser-fs-access "0.29.1"
|
browser-fs-access "0.29.1"
|
||||||
canvas-roundrect-polyfill "0.0.1"
|
canvas-roundrect-polyfill "0.0.1"
|
||||||
@@ -1796,25 +1796,32 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.1.0.tgz#6f388d6a9708cf655b8c9c6aa3fa569ee71ecf0f"
|
resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.1.0.tgz#6f388d6a9708cf655b8c9c6aa3fa569ee71ecf0f"
|
||||||
integrity sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==
|
integrity sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==
|
||||||
|
|
||||||
"@floating-ui/core@^0.7.3":
|
"@floating-ui/core@^1.6.0":
|
||||||
version "0.7.3"
|
version "1.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6"
|
||||||
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
|
integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==
|
||||||
|
|
||||||
"@floating-ui/dom@^0.5.3":
|
|
||||||
version "0.5.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1"
|
|
||||||
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/core" "^0.7.3"
|
"@floating-ui/utils" "^0.2.9"
|
||||||
|
|
||||||
"@floating-ui/react-dom@0.7.2":
|
"@floating-ui/dom@^1.0.0":
|
||||||
version "0.7.2"
|
version "1.6.13"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34"
|
||||||
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
|
integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom" "^0.5.3"
|
"@floating-ui/core" "^1.6.0"
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
"@floating-ui/utils" "^0.2.9"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^2.0.0":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
|
||||||
|
integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.0.0"
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.2.9":
|
||||||
|
version "0.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
|
||||||
|
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.3.0"
|
version "9.3.0"
|
||||||
@@ -1982,13 +1989,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/react-arrow@1.0.1":
|
"@radix-ui/primitive@1.1.1":
|
||||||
version "1.0.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz#5246adf79e97f89e819af68da51ddcf349ecf1c4"
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
|
||||||
integrity sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==
|
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
|
||||||
|
integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
|
||||||
|
|
||||||
"@radix-ui/react-collection@1.0.1":
|
"@radix-ui/react-collection@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
@@ -2008,6 +2019,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
||||||
|
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
||||||
|
|
||||||
"@radix-ui/react-context@1.0.0":
|
"@radix-ui/react-context@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
||||||
@@ -2015,6 +2031,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-context@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
|
||||||
|
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
|
||||||
|
|
||||||
"@radix-ui/react-direction@1.0.0":
|
"@radix-ui/react-direction@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
|
||||||
@@ -2022,34 +2043,30 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer@1.0.2":
|
"@radix-ui/react-dismissable-layer@1.1.5":
|
||||||
version "1.0.2"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz#f04d1061bddf00b1ca304148516b9ddc62e45fb2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
|
||||||
integrity sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==
|
integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/primitive" "1.0.0"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.0.2"
|
|
||||||
|
|
||||||
"@radix-ui/react-focus-guards@1.0.0":
|
"@radix-ui/react-focus-guards@1.1.1":
|
||||||
version "1.0.0"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
|
||||||
integrity sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==
|
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
|
|
||||||
"@radix-ui/react-focus-scope@1.0.1":
|
"@radix-ui/react-focus-scope@1.1.2":
|
||||||
version "1.0.1"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz#faea8c25f537c5a5c38c50914b63722db0e7f951"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz#c0a4519cd95c772606a82fc5b96226cd7fdd2602"
|
||||||
integrity sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==
|
integrity sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-id@1.0.0":
|
"@radix-ui/react-id@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -2059,52 +2076,57 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||||
|
|
||||||
"@radix-ui/react-popover@1.0.3":
|
"@radix-ui/react-id@1.1.0":
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.3.tgz#65ae2ee1fca2d7fd750308549eb8e0857c6160fe"
|
|
||||||
integrity sha512-YwedSukfWsyJs3/yP3yXUq44k4/JBe3jqU63Z8v2i19qZZ3dsx32oma17ztgclWPNuqp3A+Xa9UiDlZHyVX8Vg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/primitive" "1.0.0"
|
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
|
||||||
"@radix-ui/react-context" "1.0.0"
|
|
||||||
"@radix-ui/react-dismissable-layer" "1.0.2"
|
|
||||||
"@radix-ui/react-focus-guards" "1.0.0"
|
|
||||||
"@radix-ui/react-focus-scope" "1.0.1"
|
|
||||||
"@radix-ui/react-id" "1.0.0"
|
|
||||||
"@radix-ui/react-popper" "1.1.0"
|
|
||||||
"@radix-ui/react-portal" "1.0.1"
|
|
||||||
"@radix-ui/react-presence" "1.0.0"
|
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
|
||||||
"@radix-ui/react-slot" "1.0.1"
|
|
||||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
|
||||||
aria-hidden "^1.1.1"
|
|
||||||
react-remove-scroll "2.5.5"
|
|
||||||
|
|
||||||
"@radix-ui/react-popper@1.1.0":
|
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.0.tgz#2be7e4c0cd4581f54277ca33a981c9037d2a8e60"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
|
||||||
integrity sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==
|
integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
"@floating-ui/react-dom" "0.7.2"
|
|
||||||
"@radix-ui/react-arrow" "1.0.1"
|
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
|
||||||
"@radix-ui/react-context" "1.0.0"
|
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
|
||||||
"@radix-ui/react-use-rect" "1.0.0"
|
|
||||||
"@radix-ui/react-use-size" "1.0.0"
|
|
||||||
"@radix-ui/rect" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-portal@1.0.1":
|
"@radix-ui/react-popover@1.1.6":
|
||||||
version "1.0.1"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.1.tgz#169c5a50719c2bb0079cf4c91a27aa6d37e5dd33"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.6.tgz#699634dbc7899429f657bb590d71fb3ca0904087"
|
||||||
integrity sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==
|
integrity sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/react-primitive" "1.0.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-context" "1.1.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.5"
|
||||||
|
"@radix-ui/react-focus-guards" "1.1.1"
|
||||||
|
"@radix-ui/react-focus-scope" "1.1.2"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-popper" "1.2.2"
|
||||||
|
"@radix-ui/react-portal" "1.1.4"
|
||||||
|
"@radix-ui/react-presence" "1.1.2"
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
aria-hidden "^1.2.4"
|
||||||
|
react-remove-scroll "^2.6.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-popper@1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029"
|
||||||
|
integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.0.0"
|
||||||
|
"@radix-ui/react-arrow" "1.1.2"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-context" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-rect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-size" "1.1.0"
|
||||||
|
"@radix-ui/rect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@1.1.4":
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8"
|
||||||
|
integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.2"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.0.0":
|
"@radix-ui/react-presence@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -2115,6 +2137,14 @@
|
|||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-compose-refs" "1.0.0"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-presence@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
||||||
|
integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-primitive@1.0.1":
|
"@radix-ui/react-primitive@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
|
||||||
@@ -2123,6 +2153,13 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-slot" "1.0.1"
|
"@radix-ui/react-slot" "1.0.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@2.0.2":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
|
||||||
|
integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.0.2":
|
"@radix-ui/react-roving-focus@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
|
||||||
@@ -2147,6 +2184,13 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@radix-ui/react-compose-refs" "1.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-slot@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
|
||||||
|
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-tabs@1.0.2":
|
"@radix-ui/react-tabs@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
|
||||||
@@ -2169,6 +2213,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
|
||||||
|
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
|
||||||
|
|
||||||
"@radix-ui/react-use-controllable-state@1.0.0":
|
"@radix-ui/react-use-controllable-state@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
|
||||||
@@ -2177,13 +2226,19 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||||
|
|
||||||
"@radix-ui/react-use-escape-keydown@1.0.2":
|
"@radix-ui/react-use-controllable-state@1.1.0":
|
||||||
version "1.0.2"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz#09ab6455ab240b4f0a61faf06d4e5132c4d639f6"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
|
||||||
integrity sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==
|
integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
|
||||||
|
"@radix-ui/react-use-escape-keydown@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
|
||||||
|
integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-use-layout-effect@1.0.0":
|
"@radix-ui/react-use-layout-effect@1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -2192,28 +2247,29 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/react-use-rect@1.0.0":
|
"@radix-ui/react-use-layout-effect@1.1.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz#b040cc88a4906b78696cd3a32b075ed5b1423b3e"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
||||||
integrity sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==
|
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.13.10"
|
|
||||||
"@radix-ui/rect" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-use-size@1.0.0":
|
"@radix-ui/react-use-rect@1.1.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
|
||||||
integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==
|
integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/rect" "1.1.0"
|
||||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
|
||||||
|
|
||||||
"@radix-ui/rect@1.0.0":
|
"@radix-ui/react-use-size@1.1.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b"
|
||||||
integrity sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==
|
integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/rect@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
|
||||||
|
integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==
|
||||||
|
|
||||||
"@sideway/address@^4.1.3":
|
"@sideway/address@^4.1.3":
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
@@ -2952,7 +3008,7 @@ argparse@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
aria-hidden@^1.1.1:
|
aria-hidden@^1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
|
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
|
||||||
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
|
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
|
||||||
@@ -7483,7 +7539,7 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
|||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react-remove-scroll-bar@^2.3.3:
|
react-remove-scroll-bar@^2.3.7:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
|
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
|
||||||
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
|
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
|
||||||
@@ -7491,16 +7547,16 @@ react-remove-scroll-bar@^2.3.3:
|
|||||||
react-style-singleton "^2.2.2"
|
react-style-singleton "^2.2.2"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
react-remove-scroll@2.5.5:
|
react-remove-scroll@^2.6.3:
|
||||||
version "2.5.5"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
|
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz#df02cde56d5f2731e058531f8ffd7f9adec91ac2"
|
||||||
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
|
integrity sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
react-remove-scroll-bar "^2.3.3"
|
react-remove-scroll-bar "^2.3.7"
|
||||||
react-style-singleton "^2.2.1"
|
react-style-singleton "^2.2.3"
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
use-callback-ref "^1.3.0"
|
use-callback-ref "^1.3.3"
|
||||||
use-sidecar "^1.1.2"
|
use-sidecar "^1.1.3"
|
||||||
|
|
||||||
react-router-config@^5.1.1:
|
react-router-config@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
@@ -7543,7 +7599,7 @@ react-simple-code-editor@^0.10.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
|
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
|
||||||
integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==
|
integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==
|
||||||
|
|
||||||
react-style-singleton@^2.2.1, react-style-singleton@^2.2.2:
|
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||||
version "2.2.3"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
||||||
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
|
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
|
||||||
@@ -8805,7 +8861,7 @@ url-parse-lax@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prepend-http "^2.0.0"
|
prepend-http "^2.0.0"
|
||||||
|
|
||||||
use-callback-ref@^1.3.0:
|
use-callback-ref@^1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
|
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
|
||||||
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
|
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
|
||||||
@@ -8829,7 +8885,7 @@ use-latest@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
use-sidecar@^1.1.2:
|
use-sidecar@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
|
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
|
||||||
integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
|
integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
import "../common.scss";
|
import "../common.scss";
|
||||||
|
|
||||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
|
||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||||
|
|
||||||
const ExcalidrawWrapper: React.FC = () => {
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import "../common.scss";
|
import "../common.scss";
|
||||||
|
|
||||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { nanoid } from "nanoid";
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
@@ -6,13 +7,24 @@ import React, {
|
|||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
} from "react";
|
} from "react";
|
||||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
||||||
|
import type {
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
Theme,
|
||||||
|
} from "@excalidraw/excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
Gesture,
|
||||||
|
LibraryItems,
|
||||||
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { nanoid } from "nanoid";
|
import initialData from "../initialData";
|
||||||
|
|
||||||
import type { ResolvablePromise } from "../utils";
|
|
||||||
import {
|
import {
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
distance2d,
|
distance2d,
|
||||||
@@ -23,25 +35,12 @@ import {
|
|||||||
|
|
||||||
import CustomFooter from "./CustomFooter";
|
import CustomFooter from "./CustomFooter";
|
||||||
import MobileFooter from "./MobileFooter";
|
import MobileFooter from "./MobileFooter";
|
||||||
import initialData from "../initialData";
|
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||||
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
BinaryFileData,
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
Gesture,
|
|
||||||
LibraryItems,
|
|
||||||
PointerDownState as ExcalidrawPointerDownState,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type {
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
Theme,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
|
||||||
|
|
||||||
import "./ExampleApp.scss";
|
import "./ExampleApp.scss";
|
||||||
|
|
||||||
|
import type { ResolvablePromise } from "../utils";
|
||||||
|
|
||||||
type Comment = {
|
type Comment = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -105,6 +104,7 @@ export default function ExampleApp({
|
|||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
|
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
@@ -193,6 +193,7 @@ export default function ExampleApp({
|
|||||||
}) => setPointerData(payload),
|
}) => setPointerData(payload),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
renderScrollbars,
|
||||||
gridModeEnabled,
|
gridModeEnabled,
|
||||||
theme,
|
theme,
|
||||||
name: "Custom name of drawing",
|
name: "Custom name of drawing",
|
||||||
@@ -711,6 +712,14 @@ export default function ExampleApp({
|
|||||||
/>
|
/>
|
||||||
Grid mode
|
Grid mode
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={renderScrollbars}
|
||||||
|
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||||
|
/>
|
||||||
|
Render scrollbars
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
|
||||||
import CustomFooter from "./CustomFooter";
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import CustomFooter from "./CustomFooter";
|
||||||
|
|
||||||
const MobileFooter = ({
|
const MobileFooter = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import "./ExampleSidebar.scss";
|
import "./ExampleSidebar.scss";
|
||||||
|
|
||||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
|
@@ -12,9 +12,8 @@
|
|||||||
<script>
|
<script>
|
||||||
window.name = "codesandbox";
|
window.name = "codesandbox";
|
||||||
window.EXCALIDRAW_ASSET_PATH =
|
window.EXCALIDRAW_ASSET_PATH =
|
||||||
"https://esm.sh/@excalidraw/excalidraw@0.18.0-rc.5/dist/prod/";
|
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/dist/dev/index.css" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import App from "./components/ExampleApp";
|
|
||||||
import React, { StrictMode } from "react";
|
import React, { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import App from "./components/ExampleApp";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"@excalidraw/excalidraw": "0.18.0-rc.5",
|
"@excalidraw/excalidraw": "*",
|
||||||
"browser-fs-access": "0.29.1"
|
"browser-fs-access": "0.29.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:preview": "yarn build && vite preview --port 5002"
|
"preview": "vite preview --port 5002",
|
||||||
|
"build:preview": "yarn build && yarn preview",
|
||||||
|
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { unstable_batchedUpdates } from "react-dom";
|
|
||||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
|
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
|
||||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"outputDirectory": "dist",
|
"outputDirectory": "dist",
|
||||||
"installCommand": "yarn install"
|
"installCommand": "yarn install",
|
||||||
|
"buildCommand": "yarn build:package && yarn build"
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,7 @@
|
|||||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
import Slider from "rc-slider";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import "rc-slider/assets/index.css";
|
||||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
|
||||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
|
||||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
|
||||||
import {
|
|
||||||
APP_NAME,
|
|
||||||
EVENT,
|
|
||||||
THEME,
|
|
||||||
TITLE_TIMEOUT,
|
|
||||||
VERSION_TIMEOUT,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
|
||||||
import type {
|
|
||||||
FileId,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
@@ -26,15 +9,23 @@ import {
|
|||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import type {
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
AppState,
|
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
BinaryFiles,
|
|
||||||
ExcalidrawInitialDataState,
|
|
||||||
UIAppState,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
|
|
||||||
import {
|
import {
|
||||||
|
CommandPalette,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
|
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
||||||
|
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||||
|
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||||
|
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
||||||
|
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||||
|
import {
|
||||||
|
APP_NAME,
|
||||||
|
EVENT,
|
||||||
|
THEME,
|
||||||
|
TITLE_TIMEOUT,
|
||||||
|
VERSION_TIMEOUT,
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
getFrame,
|
getFrame,
|
||||||
@@ -42,75 +33,15 @@ import {
|
|||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
isRunningInIframe,
|
isRunningInIframe,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
isDevEnv,
|
||||||
import {
|
assertNever,
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
} from "@excalidraw/common";
|
||||||
isExcalidrawPlusSignedUser,
|
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||||
STORAGE_KEYS,
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||||
} from "./app_constants";
|
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
||||||
import type { CollabAPI } from "./collab/Collab";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import Collab, {
|
|
||||||
collabAPIAtom,
|
|
||||||
isCollaboratingAtom,
|
|
||||||
isOfflineAtom,
|
|
||||||
} from "./collab/Collab";
|
|
||||||
import {
|
|
||||||
exportToBackend,
|
|
||||||
getCollaborationLinkData,
|
|
||||||
isCollaborationLink,
|
|
||||||
loadScene,
|
|
||||||
} from "./data";
|
|
||||||
import {
|
|
||||||
importFromLocalStorage,
|
|
||||||
importUsernameFromLocalStorage,
|
|
||||||
} from "./data/localStorage";
|
|
||||||
import CustomStats from "./CustomStats";
|
|
||||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import {
|
|
||||||
ExportToExcalidrawPlus,
|
|
||||||
exportToExcalidrawPlus,
|
|
||||||
} from "./components/ExportToExcalidrawPlus";
|
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
|
||||||
import {
|
|
||||||
LibraryIndexedDBAdapter,
|
|
||||||
LibraryLocalStorageMigrationAdapter,
|
|
||||||
LocalData,
|
|
||||||
} from "./data/LocalData";
|
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {
|
|
||||||
parseLibraryTokensFromUrl,
|
|
||||||
useHandleLibrary,
|
|
||||||
} from "@excalidraw/excalidraw/data/library";
|
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
|
||||||
import { AppFooter } from "./components/AppFooter";
|
|
||||||
import {
|
|
||||||
Provider,
|
|
||||||
useAtom,
|
|
||||||
useAtomValue,
|
|
||||||
useAtomWithInitialValue,
|
|
||||||
appJotaiStore,
|
|
||||||
} from "./app-jotai";
|
|
||||||
|
|
||||||
import "./index.scss";
|
|
||||||
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
|
||||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
|
||||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
|
||||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
|
||||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
|
||||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
|
||||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
|
||||||
import {
|
|
||||||
CommandPalette,
|
|
||||||
DEFAULT_CATEGORIES,
|
|
||||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
|
||||||
import {
|
import {
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
XBrandIcon,
|
XBrandIcon,
|
||||||
@@ -121,6 +52,86 @@ import {
|
|||||||
share,
|
share,
|
||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
|
import { isElementLink } from "@excalidraw/element/elementLink";
|
||||||
|
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||||
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
parseLibraryTokensFromUrl,
|
||||||
|
useHandleLibrary,
|
||||||
|
} from "@excalidraw/excalidraw/data/library";
|
||||||
|
import { StoreDelta, DurableStoreIncrement, EphemeralStoreIncrement, StoreIncrement } from "@excalidraw/excalidraw/store";
|
||||||
|
|
||||||
|
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
||||||
|
import type {
|
||||||
|
FileId,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
SceneElementsMap,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
BinaryFiles,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
UIAppState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
||||||
|
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||||
|
|
||||||
|
import CustomStats from "./CustomStats";
|
||||||
|
import {
|
||||||
|
Provider,
|
||||||
|
useAtom,
|
||||||
|
useAtomValue,
|
||||||
|
useAtomWithInitialValue,
|
||||||
|
appJotaiStore,
|
||||||
|
} from "./app-jotai";
|
||||||
|
import {
|
||||||
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
|
isExcalidrawPlusSignedUser,
|
||||||
|
STORAGE_KEYS,
|
||||||
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
|
} from "./app_constants";
|
||||||
|
import Collab, {
|
||||||
|
collabAPIAtom,
|
||||||
|
isCollaboratingAtom,
|
||||||
|
isOfflineAtom,
|
||||||
|
syncApiAtom,
|
||||||
|
} from "./collab/Collab";
|
||||||
|
import { AppFooter } from "./components/AppFooter";
|
||||||
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
|
import {
|
||||||
|
ExportToExcalidrawPlus,
|
||||||
|
exportToExcalidrawPlus,
|
||||||
|
} from "./components/ExportToExcalidrawPlus";
|
||||||
|
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||||
|
|
||||||
|
import {
|
||||||
|
exportToBackend,
|
||||||
|
getCollaborationLinkData,
|
||||||
|
isCollaborationLink,
|
||||||
|
loadScene,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
|
import {
|
||||||
|
importFromLocalStorage,
|
||||||
|
importUsernameFromLocalStorage,
|
||||||
|
} from "./data/localStorage";
|
||||||
|
|
||||||
|
import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
|
import {
|
||||||
|
LibraryIndexedDBAdapter,
|
||||||
|
LibraryLocalStorageMigrationAdapter,
|
||||||
|
LocalData,
|
||||||
|
} from "./data/LocalData";
|
||||||
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
|
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
||||||
|
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
||||||
import { useHandleAppTheme } from "./useHandleAppTheme";
|
import { useHandleAppTheme } from "./useHandleAppTheme";
|
||||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||||
import { useAppLangCode } from "./app-language/language-state";
|
import { useAppLangCode } from "./app-language/language-state";
|
||||||
@@ -131,7 +142,10 @@ import DebugCanvas, {
|
|||||||
} from "./components/DebugCanvas";
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||||
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
|
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
import type { CollabAPI } from "./collab/Collab";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -362,11 +376,40 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
|
const [syncAPI] = useAtom(syncApiAtom);
|
||||||
|
const [sliderVersion, setSliderVersion] = useState(0);
|
||||||
|
const [acknowledgedDeltas, setAcknowledgedDeltas] = useState<StoreDelta[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const acknowledgedDeltasRef = useRef<StoreDelta[]>(acknowledgedDeltas);
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
return isCollaborationLink(window.location.href);
|
return isCollaborationLink(window.location.href);
|
||||||
});
|
});
|
||||||
const collabError = useAtomValue(collabErrorIndicatorAtom);
|
const collabError = useAtomValue(collabErrorIndicatorAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
acknowledgedDeltasRef.current = acknowledgedDeltas;
|
||||||
|
}, [acknowledgedDeltas]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const deltas = syncAPI?.acknowledgedDeltas ?? [];
|
||||||
|
|
||||||
|
// CFDO: buffer local deltas as well, not only acknowledged ones
|
||||||
|
if (deltas.length > acknowledgedDeltasRef.current.length) {
|
||||||
|
setAcknowledgedDeltas([...deltas]);
|
||||||
|
setSliderVersion(deltas.length);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
syncAPI?.connect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
syncAPI?.disconnect();
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [syncAPI]);
|
||||||
|
|
||||||
useHandleLibrary({
|
useHandleLibrary({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
adapter: LibraryIndexedDBAdapter,
|
adapter: LibraryIndexedDBAdapter,
|
||||||
@@ -377,7 +420,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
const [, forceRefresh] = useState(false);
|
const [, forceRefresh] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (import.meta.env.DEV) {
|
if (isDevEnv()) {
|
||||||
const debugState = loadSavedDebugState();
|
const debugState = loadSavedDebugState();
|
||||||
|
|
||||||
if (debugState.enabled && !window.visualDebug) {
|
if (debugState.enabled && !window.visualDebug) {
|
||||||
@@ -602,7 +645,13 @@ const ExcalidrawWrapper = () => {
|
|||||||
excalidrawAPI.getSceneElements(),
|
excalidrawAPI.getSceneElements(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
preventUnload(event);
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||||
@@ -663,6 +712,34 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onIncrement = (
|
||||||
|
increment: DurableStoreIncrement | EphemeralStoreIncrement,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!syncAPI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StoreIncrement.isDurable(increment)) {
|
||||||
|
// push only if there are element changes
|
||||||
|
if (!increment.delta.elements.isEmpty()) {
|
||||||
|
syncAPI.push(increment.delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StoreIncrement.isEphemeral(increment)) {
|
||||||
|
syncAPI.relay(increment.change);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNever(increment, `Unknown increment type`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error during onIncrement handler", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -785,6 +862,57 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debouncedTimeTravel = debounce(
|
||||||
|
(value: number, direction: "forward" | "backward") => {
|
||||||
|
if (!excalidrawAPI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextAppState = excalidrawAPI.getAppState();
|
||||||
|
// CFDO: retrieve the scene map already
|
||||||
|
let nextElements = new Map(
|
||||||
|
excalidrawAPI.getSceneElements().map((x) => [x.id, x]),
|
||||||
|
) as SceneElementsMap;
|
||||||
|
|
||||||
|
let deltas: StoreDelta[] = [];
|
||||||
|
|
||||||
|
// CFDO I: going both in collaborative setting means the (acknowledge) deltas need to have applied latest changes
|
||||||
|
switch (direction) {
|
||||||
|
case "forward": {
|
||||||
|
deltas = acknowledgedDeltas.slice(sliderVersion, value) ?? [];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "backward": {
|
||||||
|
deltas = acknowledgedDeltas
|
||||||
|
.slice(value)
|
||||||
|
.reverse()
|
||||||
|
.map((x) => StoreDelta.inverse(x));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
assertNever(direction, `Unknown direction: ${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const delta of deltas) {
|
||||||
|
[nextElements, nextAppState] = excalidrawAPI.store.applyDeltaTo(
|
||||||
|
delta,
|
||||||
|
nextElements,
|
||||||
|
nextAppState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
excalidrawAPI?.updateScene({
|
||||||
|
appState: {
|
||||||
|
...nextAppState,
|
||||||
|
viewModeEnabled: value !== acknowledgedDeltas.length,
|
||||||
|
},
|
||||||
|
elements: Array.from(nextElements.values()),
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
@@ -792,9 +920,45 @@ const ExcalidrawWrapper = () => {
|
|||||||
"is-collaborating": isCollaborating,
|
"is-collaborating": isCollaborating,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<Slider
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "25px",
|
||||||
|
zIndex: 999,
|
||||||
|
width: "60%",
|
||||||
|
left: "25%",
|
||||||
|
}}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={acknowledgedDeltas.length}
|
||||||
|
value={sliderVersion}
|
||||||
|
onChange={(value) => {
|
||||||
|
const nextSliderVersion = value as number;
|
||||||
|
// CFDO: in safari the whole canvas gets selected when dragging
|
||||||
|
if (nextSliderVersion !== acknowledgedDeltas.length) {
|
||||||
|
// don't listen to updates in the detached mode
|
||||||
|
syncAPI?.disconnect();
|
||||||
|
} else {
|
||||||
|
// reconnect once we're back to the latest version
|
||||||
|
syncAPI?.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSliderVersion === sliderVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedTimeTravel(
|
||||||
|
nextSliderVersion,
|
||||||
|
nextSliderVersion < sliderVersion ? "backward" : "forward",
|
||||||
|
);
|
||||||
|
|
||||||
|
setSliderVersion(nextSliderVersion);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
excalidrawAPI={excalidrawRefCallback}
|
excalidrawAPI={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onIncrement={onIncrement}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
@@ -873,7 +1037,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
/>
|
/>
|
||||||
<OverwriteConfirmDialog>
|
<OverwriteConfirmDialog>
|
||||||
<OverwriteConfirmDialog.Actions.ExportToImage />
|
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||||
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
|
||||||
{excalidrawAPI && (
|
{excalidrawAPI && (
|
||||||
<OverwriteConfirmDialog.Action
|
<OverwriteConfirmDialog.Action
|
||||||
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
||||||
|
@@ -1,15 +1,21 @@
|
|||||||
|
import { Stats } from "@excalidraw/excalidraw";
|
||||||
|
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||||
|
import {
|
||||||
|
DEFAULT_VERSION,
|
||||||
|
debounce,
|
||||||
|
getVersion,
|
||||||
|
nFormatter,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
|
|
||||||
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementsStorageSize,
|
getElementsStorageSize,
|
||||||
getTotalStorageSize,
|
getTotalStorageSize,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import { Stats } from "@excalidraw/excalidraw";
|
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
|
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
||||||
|
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
||||||
import { useLayoutEffect, useRef } from "react";
|
import { useLayoutEffect, useRef } from "react";
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
|
||||||
import { LocalData } from "./data/LocalData";
|
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
||||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
|
||||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
import { LocalData } from "./data/LocalData";
|
||||||
|
|
||||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
|
||||||
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { useSetAtom } from "../app-jotai";
|
import { useSetAtom } from "../app-jotai";
|
||||||
|
|
||||||
import { appLangCodeAtom } from "./language-state";
|
import { appLangCodeAtom } from "./language-state";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
export const languageDetector = new LanguageDetector();
|
export const languageDetector = new LanguageDetector();
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { atom, useAtom } from "../app-jotai";
|
import { atom, useAtom } from "../app-jotai";
|
||||||
|
|
||||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||||
|
|
||||||
export const appLangCodeAtom = atom(getPreferredLanguage());
|
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||||
|
@@ -45,6 +45,7 @@ export const STORAGE_KEYS = {
|
|||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
|
|
||||||
IDB_LIBRARY: "excalidraw-library",
|
IDB_LIBRARY: "excalidraw-library",
|
||||||
|
IDB_SYNC: "excalidraw-sync",
|
||||||
|
|
||||||
// do not use apart from migrations
|
// do not use apart from migrations
|
||||||
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
|
@@ -1,21 +1,3 @@
|
|||||||
import throttle from "lodash.throttle";
|
|
||||||
import { PureComponent } from "react";
|
|
||||||
import type {
|
|
||||||
BinaryFileData,
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
SocketId,
|
|
||||||
Collaborator,
|
|
||||||
Gesture,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
|
||||||
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
InitializedExcalidrawImageElement,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import {
|
import {
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
@@ -23,12 +5,54 @@ import {
|
|||||||
zoomToFitBounds,
|
zoomToFitBounds,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
|
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||||
|
import { APP_NAME, EVENT } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
|
IDLE_THRESHOLD,
|
||||||
|
ACTIVE_THRESHOLD,
|
||||||
|
UserIdleState,
|
||||||
assertNever,
|
assertNever,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
} from "@excalidraw/common";
|
||||||
|
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
|
import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
|
||||||
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
|
import {
|
||||||
|
isImageElement,
|
||||||
|
isInitializedImageElement,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||||
|
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import { PureComponent } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ReconciledExcalidrawElement,
|
||||||
|
RemoteExcalidrawElement,
|
||||||
|
} from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FileId,
|
||||||
|
InitializedExcalidrawImageElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
BinaryFileData,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
SocketId,
|
||||||
|
Collaborator,
|
||||||
|
Gesture,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { appJotaiStore, atom } from "../app-jotai";
|
||||||
import {
|
import {
|
||||||
CURSOR_SYNC_TIMEOUT,
|
CURSOR_SYNC_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
@@ -39,15 +63,17 @@ import {
|
|||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
WS_EVENTS,
|
WS_EVENTS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import type {
|
|
||||||
SocketUpdateDataSource,
|
|
||||||
SyncableExcalidrawElement,
|
|
||||||
} from "../data";
|
|
||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
|
import {
|
||||||
|
encodeFilesForUpload,
|
||||||
|
FileManager,
|
||||||
|
updateStaleImageStatuses,
|
||||||
|
} from "../data/FileManager";
|
||||||
|
import { LocalData, SyncIndexedDBAdapter } from "../data/LocalData";
|
||||||
import {
|
import {
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
loadFilesFromFirebase,
|
loadFilesFromFirebase,
|
||||||
@@ -59,37 +85,17 @@ import {
|
|||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
saveUsernameToLocalStorage,
|
saveUsernameToLocalStorage,
|
||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
IDLE_THRESHOLD,
|
|
||||||
ACTIVE_THRESHOLD,
|
|
||||||
UserIdleState,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import {
|
|
||||||
encodeFilesForUpload,
|
|
||||||
FileManager,
|
|
||||||
updateStaleImageStatuses,
|
|
||||||
} from "../data/FileManager";
|
|
||||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
|
||||||
import {
|
|
||||||
isImageElement,
|
|
||||||
isInitializedImageElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
|
||||||
import { appJotaiStore, atom } from "../app-jotai";
|
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
|
||||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
|
||||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
|
||||||
import type {
|
|
||||||
ReconciledExcalidrawElement,
|
|
||||||
RemoteExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/data/reconcile";
|
|
||||||
|
|
||||||
|
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||||
|
import Portal from "./Portal";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SocketUpdateDataSource,
|
||||||
|
SyncableExcalidrawElement,
|
||||||
|
} from "../data";
|
||||||
|
|
||||||
|
export const syncApiAtom = atom<SyncClient | null>(null);
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const isCollaboratingAtom = atom(false);
|
export const isCollaboratingAtom = atom(false);
|
||||||
export const isOfflineAtom = atom(false);
|
export const isOfflineAtom = atom(false);
|
||||||
@@ -236,7 +242,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
SyncClient.create(this.excalidrawAPI, SyncIndexedDBAdapter).then(
|
||||||
|
(syncAPI) => {
|
||||||
|
appJotaiStore.set(syncApiAtom, syncAPI);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
Object.defineProperties(window, {
|
Object.defineProperties(window, {
|
||||||
collab: {
|
collab: {
|
||||||
@@ -269,6 +281,8 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
window.clearTimeout(this.idleTimeoutId);
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
this.idleTimeoutId = null;
|
this.idleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appJotaiStore.get(syncApiAtom)?.disconnect();
|
||||||
this.onUmmount?.();
|
this.onUmmount?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +310,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
// the purpose is to run in immediately after user decides to stay
|
// the purpose is to run in immediately after user decides to stay
|
||||||
this.saveCollabRoomToFirebase(syncableElements);
|
this.saveCollabRoomToFirebase(syncableElements);
|
||||||
|
|
||||||
preventUnload(event);
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1009,7 +1029,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
|||||||
import { warning } from "@excalidraw/excalidraw/components/icons";
|
import { warning } from "@excalidraw/excalidraw/components/icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { atom } from "../app-jotai";
|
import { atom } from "../app-jotai";
|
||||||
|
|
||||||
import "./CollabError.scss";
|
import "./CollabError.scss";
|
||||||
|
@@ -1,25 +1,26 @@
|
|||||||
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
|
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
|
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
OnUserFollowedPayload,
|
||||||
|
SocketId,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||||
|
import { isSyncableElement } from "../data";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
SocketUpdateData,
|
SocketUpdateData,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import { isSyncableElement } from "../data";
|
|
||||||
|
|
||||||
import type { TCollabClass } from "./Collab";
|
import type { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
|
||||||
import type {
|
|
||||||
OnUserFollowedPayload,
|
|
||||||
SocketId,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
|
||||||
import throttle from "lodash.throttle";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: TCollabClass;
|
collab: TCollabClass;
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
|
||||||
import {
|
import {
|
||||||
DiagramToCodePlugin,
|
DiagramToCodePlugin,
|
||||||
exportToBlob,
|
exportToBlob,
|
||||||
@@ -7,7 +6,9 @@ import {
|
|||||||
TTDDialog,
|
TTDDialog,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
||||||
import { safelyParseJSON } from "@excalidraw/excalidraw/utils";
|
import { safelyParseJSON } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
export const AIComponents = ({
|
export const AIComponents = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import { Footer } from "@excalidraw/excalidraw/index";
|
import { Footer } from "@excalidraw/excalidraw/index";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
|
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||||
import { EncryptedIcon } from "./EncryptedIcon";
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
|
||||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
|
||||||
|
|
||||||
export const AppFooter = React.memo(
|
export const AppFooter = React.memo(
|
||||||
({ onChange }: { onChange: () => void }) => {
|
({ onChange }: { onChange: () => void }) => {
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
loginIcon,
|
loginIcon,
|
||||||
ExcalLogo,
|
ExcalLogo,
|
||||||
eyeIcon,
|
eyeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import React from "react";
|
||||||
|
|
||||||
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
import { saveDebugState } from "./DebugCanvas";
|
import { saveDebugState } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
@@ -54,7 +59,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
>
|
>
|
||||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
{import.meta.env.DEV && (
|
{isDevEnv() && (
|
||||||
<MainMenu.Item
|
<MainMenu.Item
|
||||||
icon={eyeIcon}
|
icon={eyeIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
|
||||||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
||||||
|
import { POINTER_EVENTS } from "@excalidraw/common";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
|
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
|
@@ -1,24 +1,28 @@
|
|||||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
|
||||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import { throttleRAF } from "@excalidraw/excalidraw/utils";
|
|
||||||
import {
|
|
||||||
bootstrapCanvas,
|
|
||||||
getNormalizedCanvasDimensions,
|
|
||||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
|
||||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
|
||||||
import {
|
import {
|
||||||
ArrowheadArrowIcon,
|
ArrowheadArrowIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import {
|
||||||
import type { Curve } from "../../packages/math";
|
bootstrapCanvas,
|
||||||
|
getNormalizedCanvasDimensions,
|
||||||
|
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||||
|
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
import { throttleRAF } from "@excalidraw/common";
|
||||||
|
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LineSegment,
|
type LineSegment,
|
||||||
} from "../../packages/math";
|
} from "@excalidraw/math";
|
||||||
import { isCurve } from "../../packages/math/curve";
|
import { isCurve } from "@excalidraw/math/curve";
|
||||||
|
|
||||||
|
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||||
|
|
||||||
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { shield } from "@excalidraw/excalidraw/components/icons";
|
|
||||||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||||
|
import { shield } from "@excalidraw/excalidraw/components/icons";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
export const EncryptedIcon = () => {
|
export const EncryptedIcon = () => {
|
||||||
|
@@ -1,31 +1,33 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { uploadBytes, ref } from "firebase/storage";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { Card } from "@excalidraw/excalidraw/components/Card";
|
import { Card } from "@excalidraw/excalidraw/components/Card";
|
||||||
|
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
||||||
|
import { MIME_TYPES, getFrame } from "@excalidraw/common";
|
||||||
|
import {
|
||||||
|
encryptData,
|
||||||
|
generateEncryptionKey,
|
||||||
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
encryptData,
|
|
||||||
generateEncryptionKey,
|
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "../data/FileManager";
|
import { encodeFilesForUpload } from "../data/FileManager";
|
||||||
import { uploadBytes, ref } from "firebase/storage";
|
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
|
||||||
|
|
||||||
export const exportToExcalidrawPlus = async (
|
export const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
|
import { THEME } from "@excalidraw/common";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { THEME } from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface TopErrorBoundaryState {
|
interface TopErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import type {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
|
@@ -10,6 +10,13 @@
|
|||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
||||||
|
import {
|
||||||
|
CANVAS_SEARCH_TAB,
|
||||||
|
DEFAULT_SIDEBAR,
|
||||||
|
debounce,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
createStore,
|
createStore,
|
||||||
entries,
|
entries,
|
||||||
@@ -19,26 +26,21 @@ import {
|
|||||||
setMany,
|
setMany,
|
||||||
get,
|
get,
|
||||||
} from "idb-keyval";
|
} from "idb-keyval";
|
||||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
|
||||||
import {
|
import { StoreDelta } from "@excalidraw/excalidraw/store";
|
||||||
CANVAS_SEARCH_TAB,
|
|
||||||
DEFAULT_SIDEBAR,
|
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types";
|
import type { DTO, MaybePromise } from "@excalidraw/common/utility-types";
|
||||||
import { debounce } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
import { FileManager } from "./FileManager";
|
import { FileManager } from "./FileManager";
|
||||||
import { Locker } from "./Locker";
|
import { Locker } from "./Locker";
|
||||||
import { updateBrowserStateVersion } from "./tabSync";
|
import { updateBrowserStateVersion } from "./tabSync";
|
||||||
@@ -104,13 +106,12 @@ export class LocalData {
|
|||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
onFilesSaved: () => void,
|
onFilesSaved: () => void,
|
||||||
) => {
|
) => {
|
||||||
saveDataStateToLocalStorage(elements, appState);
|
// saveDataStateToLocalStorage(elements, appState);
|
||||||
|
// await this.fileStorage.saveFiles({
|
||||||
await this.fileStorage.saveFiles({
|
// elements,
|
||||||
elements,
|
// files,
|
||||||
files,
|
// });
|
||||||
});
|
// onFilesSaved();
|
||||||
onFilesSaved();
|
|
||||||
},
|
},
|
||||||
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
||||||
);
|
);
|
||||||
@@ -256,3 +257,60 @@ export class LibraryLocalStorageMigrationAdapter {
|
|||||||
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SyncDeltaPersistedData = DTO<StoreDelta>[];
|
||||||
|
|
||||||
|
type SyncMetaPersistedData = {
|
||||||
|
lastAcknowledgedVersion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SyncIndexedDBAdapter {
|
||||||
|
/** IndexedDB database and store name */
|
||||||
|
private static idb_name = STORAGE_KEYS.IDB_SYNC;
|
||||||
|
/** library data store keys */
|
||||||
|
private static deltasKey = "deltas";
|
||||||
|
private static metadataKey = "metadata";
|
||||||
|
|
||||||
|
private static store = createStore(
|
||||||
|
`${SyncIndexedDBAdapter.idb_name}-db`,
|
||||||
|
`${SyncIndexedDBAdapter.idb_name}-store`,
|
||||||
|
);
|
||||||
|
|
||||||
|
static async loadDeltas() {
|
||||||
|
const deltas = await get<SyncDeltaPersistedData>(
|
||||||
|
SyncIndexedDBAdapter.deltasKey,
|
||||||
|
SyncIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deltas?.length) {
|
||||||
|
return deltas.map((storeDeltaDTO) => StoreDelta.restore(storeDeltaDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveDeltas(data: SyncDeltaPersistedData): Promise<void> {
|
||||||
|
return set(
|
||||||
|
SyncIndexedDBAdapter.deltasKey,
|
||||||
|
data,
|
||||||
|
SyncIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadMetadata() {
|
||||||
|
const metadata = await get<SyncMetaPersistedData>(
|
||||||
|
SyncIndexedDBAdapter.metadataKey,
|
||||||
|
SyncIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
|
||||||
|
return metadata || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveMetadata(data: SyncMetaPersistedData): Promise<void> {
|
||||||
|
return set(
|
||||||
|
SyncIndexedDBAdapter.metadataKey,
|
||||||
|
data,
|
||||||
|
SyncIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,27 +1,12 @@
|
|||||||
import { reconcileElements } from "@excalidraw/excalidraw";
|
import { reconcileElements } from "@excalidraw/excalidraw";
|
||||||
import type {
|
import { MIME_TYPES } from "@excalidraw/common";
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
OrderedExcalidrawElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import { getSceneVersion } from "@excalidraw/excalidraw/element";
|
|
||||||
import type Portal from "../collab/Portal";
|
|
||||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
BinaryFileData,
|
|
||||||
BinaryFileMetadata,
|
|
||||||
DataURL,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
|
||||||
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import {
|
import {
|
||||||
encryptData,
|
encryptData,
|
||||||
decryptData,
|
decryptData,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
||||||
import type { SyncableExcalidrawElement } from ".";
|
import { getSceneVersion } from "@excalidraw/element";
|
||||||
import { getSyncableElements } from ".";
|
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "firebase/app";
|
||||||
import {
|
import {
|
||||||
getFirestore,
|
getFirestore,
|
||||||
@@ -31,8 +16,27 @@ import {
|
|||||||
Bytes,
|
Bytes,
|
||||||
} from "firebase/firestore";
|
} from "firebase/firestore";
|
||||||
import { getStorage, ref, uploadBytes } from "firebase/storage";
|
import { getStorage, ref, uploadBytes } from "firebase/storage";
|
||||||
import type { Socket } from "socket.io-client";
|
|
||||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FileId,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
|
BinaryFileMetadata,
|
||||||
|
DataURL,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||||
|
|
||||||
|
import { getSyncableElements } from ".";
|
||||||
|
|
||||||
|
import type { SyncableExcalidrawElement } from ".";
|
||||||
|
import type Portal from "../collab/Portal";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@@ -9,34 +9,38 @@ import {
|
|||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { restore } from "@excalidraw/excalidraw/data/restore";
|
import { restore } from "@excalidraw/excalidraw/data/restore";
|
||||||
|
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
||||||
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { bytesToHexString } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
import type { SceneBounds } from "@excalidraw/element/bounds";
|
||||||
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
|
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
SocketId,
|
SocketId,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
import type { MakeBrand } from "@excalidraw/common/utility-types";
|
||||||
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
|
|
||||||
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
|
|
||||||
import type { WS_SUBTYPES } from "../app_constants";
|
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
ROOM_ID_BYTES,
|
ROOM_ID_BYTES,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
|
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
import { encodeFilesForUpload } from "./FileManager";
|
||||||
import { saveFilesToFirebase } from "./firebase";
|
import { saveFilesToFirebase } from "./firebase";
|
||||||
|
|
||||||
|
import type { WS_SUBTYPES } from "../app_constants";
|
||||||
|
|
||||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||||
MakeBrand<"SyncableExcalidrawElement">;
|
MakeBrand<"SyncableExcalidrawElement">;
|
||||||
|
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
|
||||||
import {
|
import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "@excalidraw/excalidraw/appState";
|
} from "@excalidraw/excalidraw/appState";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import ExcalidrawApp from "./App";
|
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
import "../excalidraw-app/sentry";
|
import "../excalidraw-app/sentry";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "./App";
|
||||||
|
|
||||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
||||||
const rootElement = document.getElementById("root")!;
|
const rootElement = document.getElementById("root")!;
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
@@ -33,6 +33,7 @@
|
|||||||
"i18next-browser-languagedetector": "6.1.4",
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
"idb-keyval": "6.0.3",
|
"idb-keyval": "6.0.3",
|
||||||
"jotai": "2.11.0",
|
"jotai": "2.11.0",
|
||||||
|
"rc-slider": "11.1.7",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"socket.io-client": "4.7.2",
|
"socket.io-client": "4.7.2",
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import { KEYS } from "@excalidraw/excalidraw/keys";
|
|
||||||
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
|
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
|
||||||
|
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
||||||
|
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
||||||
import {
|
import {
|
||||||
copyIcon,
|
copyIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@@ -14,16 +12,19 @@ import {
|
|||||||
shareIOS,
|
shareIOS,
|
||||||
shareWindows,
|
shareWindows,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
|
||||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
|
||||||
import type { CollabAPI } from "../collab/Collab";
|
|
||||||
import { activeRoomLinkAtom } from "../collab/Collab";
|
|
||||||
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
||||||
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
||||||
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { KEYS, getFrame } from "@excalidraw/common";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
||||||
|
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||||
|
|
||||||
import "./ShareDialog.scss";
|
import "./ShareDialog.scss";
|
||||||
|
|
||||||
|
import type { CollabAPI } from "../collab/Collab";
|
||||||
|
|
||||||
type OnExportToBackend = () => void;
|
type OnExportToBackend = () => void;
|
||||||
type ShareDialogType = "share" | "collaborationOnly";
|
type ShareDialogType = "share" | "collaborationOnly";
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import ExcalidrawApp from "../App";
|
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import {
|
import {
|
||||||
mockBoundingClientRect,
|
mockBoundingClientRect,
|
||||||
render,
|
render,
|
||||||
restoreOriginalGetBoundingClientRect,
|
restoreOriginalGetBoundingClientRect,
|
||||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import ExcalidrawApp from "../App";
|
||||||
|
|
||||||
describe("Test MobileMenu", () => {
|
describe("Test MobileMenu", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import { vi } from "vitest";
|
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
||||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
|
||||||
import ExcalidrawApp from "../App";
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
|
||||||
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
|
|
||||||
import {
|
import {
|
||||||
createRedoAction,
|
createRedoAction,
|
||||||
createUndoAction,
|
createUndoAction,
|
||||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../App";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoAction = createUndoAction(h.history, h.store);
|
const undoAction = createUndoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||||
@@ -153,7 +154,7 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const redoAction = createRedoAction(h.history, h.store);
|
const redoAction = createRedoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
// with explicit redo (as removal) we again restore the element from the snapshot!
|
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useLayoutEffect, useState } from "react";
|
|
||||||
import { THEME } from "@excalidraw/excalidraw";
|
import { THEME } from "@excalidraw/excalidraw";
|
||||||
import { EVENT } from "@excalidraw/excalidraw/constants";
|
import { EVENT, CODES, KEYS } from "@excalidraw/common";
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
|
|
||||||
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
||||||
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
||||||
|
@@ -23,29 +23,57 @@ export default defineConfig(({ mode }) => {
|
|||||||
envDir: "../",
|
envDir: "../",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [
|
alias: [
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/common/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/common/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/element/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/element/src/$1"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw$/,
|
find: /^@excalidraw\/excalidraw$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/excalidraw/index.tsx",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils$/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils\/(.*?)/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/$1"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math$/,
|
find: /^@excalidraw\/math$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/index.ts"),
|
replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math\/(.*?)/,
|
find: /^@excalidraw\/math\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/$1"),
|
replacement: path.resolve(__dirname, "../packages/math/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/utils/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -197,7 +225,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
id:"excalidraw",
|
id: "excalidraw",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
theme_color: "#121212",
|
theme_color: "#121212",
|
||||||
background_color: "#ffffff",
|
background_color: "#ffffff",
|
||||||
|
@@ -4,9 +4,7 @@
|
|||||||
"packageManager": "yarn@1.22.22",
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/*",
|
||||||
"packages/utils",
|
|
||||||
"packages/math",
|
|
||||||
"examples/*"
|
"examples/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -26,6 +24,7 @@
|
|||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-config-react-app": "7.0.1",
|
"eslint-config-react-app": "7.0.1",
|
||||||
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
|
3
packages/common/.eslintrc.json
Normal file
3
packages/common/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../eslintrc.base.json"]
|
||||||
|
}
|
19
packages/common/README.md
Normal file
19
packages/common/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @excalidraw/common
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/common
|
||||||
|
```
|
3
packages/common/global.d.ts
vendored
Normal file
3
packages/common/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
import "@excalidraw/excalidraw/global";
|
||||||
|
import "@excalidraw/excalidraw/css";
|
56
packages/common/package.json
Normal file
56
packages/common/package.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "@excalidraw/common",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/types/common/src/index.d.ts",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/types/common/src/index.d.ts",
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"production": "./dist/prod/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./../common/dist/types/common/src/*.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw common functions, constants, etc.",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-utils"
|
||||||
|
],
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all",
|
||||||
|
"not safari < 12",
|
||||||
|
"not kaios <= 2.5",
|
||||||
|
"not edge < 79",
|
||||||
|
"not chrome < 70",
|
||||||
|
"not and_uc < 13",
|
||||||
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
export default class BinaryHeap<T> {
|
export class BinaryHeap<T> {
|
||||||
private content: T[] = [];
|
private content: T[] = [];
|
||||||
|
|
||||||
constructor(private scoreFunction: (node: T) => number) {}
|
constructor(private scoreFunction: (node: T) => number) {}
|
@@ -1,6 +1,9 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
|
||||||
|
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
source: R,
|
source: R,
|
@@ -1,5 +1,9 @@
|
|||||||
import type { AppProps, AppState } from "./types";
|
import type {
|
||||||
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
ExcalidrawElement,
|
||||||
|
FontFamilyValues,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
|
|
||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
@@ -108,6 +112,7 @@ export const YOUTUBE_STATES = {
|
|||||||
export const ENV = {
|
export const ENV = {
|
||||||
TEST: "test",
|
TEST: "test",
|
||||||
DEVELOPMENT: "development",
|
DEVELOPMENT: "development",
|
||||||
|
PRODUCTION: "production",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
@@ -314,6 +319,9 @@ export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
|||||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
export const SVG_DOCUMENT_PREAMBLE = `<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
`;
|
||||||
|
|
||||||
export const ENCRYPTION_KEY_BITS = 128;
|
export const ENCRYPTION_KEY_BITS = 128;
|
||||||
|
|
||||||
@@ -415,6 +423,7 @@ export const LIBRARY_DISABLED_TYPES = new Set([
|
|||||||
// use these constants to easily identify reference sites
|
// use these constants to easily identify reference sites
|
||||||
export const TOOL_TYPE = {
|
export const TOOL_TYPE = {
|
||||||
selection: "selection",
|
selection: "selection",
|
||||||
|
lasso: "lasso",
|
||||||
rectangle: "rectangle",
|
rectangle: "rectangle",
|
||||||
diamond: "diamond",
|
diamond: "diamond",
|
||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
@@ -1,11 +1,9 @@
|
|||||||
import type { JSX } from "react";
|
import type {
|
||||||
import {
|
ExcalidrawTextElement,
|
||||||
FreedrawIcon,
|
FontFamilyValues,
|
||||||
FontFamilyNormalIcon,
|
} from "@excalidraw/element/types";
|
||||||
FontFamilyHeadingIcon,
|
|
||||||
FontFamilyCodeIcon,
|
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
|
||||||
} from "../components/icons";
|
|
||||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates font metrics with additional font metadata.
|
* Encapsulates font metrics with additional font metadata.
|
||||||
@@ -22,8 +20,6 @@ export interface FontMetadata {
|
|||||||
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
||||||
lineHeight: number;
|
lineHeight: number;
|
||||||
};
|
};
|
||||||
/** element to be displayed as an icon */
|
|
||||||
icon?: JSX.Element;
|
|
||||||
/** flag to indicate a deprecated font */
|
/** flag to indicate a deprecated font */
|
||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
/** flag to indicate a server-side only font */
|
/** flag to indicate a server-side only font */
|
||||||
@@ -42,7 +38,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -374,
|
descender: -374,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FreedrawIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Nunito]: {
|
[FONT_FAMILY.Nunito]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -51,7 +46,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -353,
|
descender: -353,
|
||||||
lineHeight: 1.35,
|
lineHeight: 1.35,
|
||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Lilita One"]]: {
|
[FONT_FAMILY["Lilita One"]]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -60,7 +54,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -220,
|
descender: -220,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
},
|
},
|
||||||
icon: FontFamilyHeadingIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Comic Shanns"]]: {
|
[FONT_FAMILY["Comic Shanns"]]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -69,7 +62,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -250,
|
descender: -250,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FontFamilyCodeIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Virgil]: {
|
[FONT_FAMILY.Virgil]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -78,7 +70,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -374,
|
descender: -374,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FreedrawIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Helvetica]: {
|
[FONT_FAMILY.Helvetica]: {
|
||||||
@@ -88,7 +79,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -471,
|
descender: -471,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
local: true,
|
local: true,
|
||||||
},
|
},
|
||||||
@@ -99,7 +89,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -480,
|
descender: -480,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
},
|
},
|
||||||
icon: FontFamilyCodeIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Liberation Sans"]]: {
|
[FONT_FAMILY["Liberation Sans"]]: {
|
||||||
@@ -148,3 +137,34 @@ export const GOOGLE_FONTS_RANGES = {
|
|||||||
|
|
||||||
/** local protocol to skip the local font from registering or inlining */
|
/** local protocol to skip the local font from registering or inlining */
|
||||||
export const LOCAL_FONT_PROTOCOL = "local:";
|
export const LOCAL_FONT_PROTOCOL = "local:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates vertical offset for a text with alphabetic baseline.
|
||||||
|
*/
|
||||||
|
export const getVerticalOffset = (
|
||||||
|
fontFamily: ExcalidrawTextElement["fontFamily"],
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeightPx: number,
|
||||||
|
) => {
|
||||||
|
const { unitsPerEm, ascender, descender } =
|
||||||
|
FONT_METADATA[fontFamily]?.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||||
|
|
||||||
|
const fontSizeEm = fontSize / unitsPerEm;
|
||||||
|
const lineGap =
|
||||||
|
(lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
|
||||||
|
|
||||||
|
const verticalOffset = fontSizeEm * ascender + lineGap;
|
||||||
|
return verticalOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets line height for a selected family.
|
||||||
|
*/
|
||||||
|
export const getLineHeight = (fontFamily: FontFamilyValues) => {
|
||||||
|
const { lineHeight } =
|
||||||
|
FONT_METADATA[fontFamily]?.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||||
|
|
||||||
|
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
11
packages/common/src/index.ts
Normal file
11
packages/common/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export * from "./binary-heap";
|
||||||
|
export * from "./colors";
|
||||||
|
export * from "./constants";
|
||||||
|
export * from "./font-metadata";
|
||||||
|
export * from "./queue";
|
||||||
|
export * from "./keys";
|
||||||
|
export * from "./points";
|
||||||
|
export * from "./promise-pool";
|
||||||
|
export * from "./random";
|
||||||
|
export * from "./url";
|
||||||
|
export * from "./utils";
|
@@ -1,4 +1,5 @@
|
|||||||
import { isDarwin } from "./constants";
|
import { isDarwin } from "./constants";
|
||||||
|
|
||||||
import type { ValueOf } from "./utility-types";
|
import type { ValueOf } from "./utility-types";
|
||||||
|
|
||||||
export const CODES = {
|
export const CODES = {
|
@@ -4,6 +4,8 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
export const getSizeFromPoints = (
|
export const getSizeFromPoints = (
|
||||||
points: readonly (GlobalPoint | LocalPoint)[],
|
points: readonly (GlobalPoint | LocalPoint)[],
|
||||||
) => {
|
) => {
|
||||||
@@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
|
|
||||||
return nextPoints;
|
return nextPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Rounding this point causes some shake when free drawing
|
||||||
|
export const getGridPoint = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
gridSize: NullableGridSize,
|
||||||
|
): [number, number] => {
|
||||||
|
if (gridSize) {
|
||||||
|
return [
|
||||||
|
Math.round(x / gridSize) * gridSize,
|
||||||
|
Math.round(y / gridSize) * gridSize,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
};
|
50
packages/common/src/promise-pool.ts
Normal file
50
packages/common/src/promise-pool.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Pool from "es6-promise-pool";
|
||||||
|
|
||||||
|
// extending the missing types
|
||||||
|
// relying on the [Index, T] to keep a correct order
|
||||||
|
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
||||||
|
addEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => (event: { data: { result: [Index, T] } }) => void;
|
||||||
|
removeEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PromisePool<T> {
|
||||||
|
private readonly pool: TPromisePool<T>;
|
||||||
|
private readonly entries: Record<number, T> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
||||||
|
concurrency: number,
|
||||||
|
) {
|
||||||
|
this.pool = new Pool(
|
||||||
|
source as unknown as () => void | PromiseLike<[number, T][]>,
|
||||||
|
concurrency,
|
||||||
|
) as TPromisePool<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public all() {
|
||||||
|
const listener = (event: { data: { result: void | [number, T] } }) => {
|
||||||
|
if (event.data.result) {
|
||||||
|
// by default pool does not return the results, so we are gathering them manually
|
||||||
|
// with the correct call order (represented by the index in the tuple)
|
||||||
|
const [index, value] = event.data.result;
|
||||||
|
this.entries[index] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pool.addEventListener("fulfilled", listener);
|
||||||
|
|
||||||
|
return this.pool.start().then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.pool.removeEventListener("fulfilled", listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(this.entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
|
import { promiseTry, resolvablePromise } from ".";
|
||||||
|
|
||||||
|
import type { ResolvablePromise } from ".";
|
||||||
|
|
||||||
import type { MaybePromise } from "./utility-types";
|
import type { MaybePromise } from "./utility-types";
|
||||||
import type { ResolvablePromise } from "./utils";
|
|
||||||
import { promiseTry, resolvablePromise } from "./utils";
|
|
||||||
|
|
||||||
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
|
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
|
||||||
|
|
@@ -1,5 +1,6 @@
|
|||||||
import { Random } from "roughjs/bin/math";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { Random } from "roughjs/bin/math";
|
||||||
|
|
||||||
import { isTestEnv } from "./utils";
|
import { isTestEnv } from "./utils";
|
||||||
|
|
||||||
let random = new Random(Date.now());
|
let random = new Random(Date.now());
|
@@ -1,5 +1,6 @@
|
|||||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
import { escapeDoubleQuotes } from "../utils";
|
|
||||||
|
import { escapeDoubleQuotes } from "./utils";
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
export const normalizeLink = (link: string) => {
|
||||||
link = link.trim();
|
link = link.trim();
|
@@ -66,5 +66,10 @@ export type MakeBrand<T extends string> = {
|
|||||||
/** Maybe just promise or already fulfilled one! */
|
/** Maybe just promise or already fulfilled one! */
|
||||||
export type MaybePromise<T> = T | Promise<T>;
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
/** Strip all the methods or functions from a type */
|
||||||
|
export type DTO<T> = {
|
||||||
|
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
// get union of all keys from the union of types
|
// get union of all keys from the union of types
|
||||||
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
@@ -1,28 +1,34 @@
|
|||||||
import Pool from "es6-promise-pool";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
import { average } from "@excalidraw/math";
|
|
||||||
import { COLOR_PALETTE } from "./colors";
|
|
||||||
import type { EVENT } from "./constants";
|
|
||||||
import {
|
|
||||||
DEFAULT_VERSION,
|
|
||||||
FONT_FAMILY,
|
|
||||||
getFontFamilyFallbacks,
|
|
||||||
isDarwin,
|
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
|
||||||
} from "./constants";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
} from "./element/types";
|
ExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ActiveTool,
|
ActiveTool,
|
||||||
AppState,
|
AppState,
|
||||||
ToolType,
|
ToolType,
|
||||||
UnsubscribeCallback,
|
UnsubscribeCallback,
|
||||||
Zoom,
|
Zoom,
|
||||||
} from "./types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { COLOR_PALETTE } from "./colors";
|
||||||
|
import {
|
||||||
|
DEFAULT_VERSION,
|
||||||
|
ENV,
|
||||||
|
FONT_FAMILY,
|
||||||
|
getFontFamilyFallbacks,
|
||||||
|
isDarwin,
|
||||||
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
|
} from "./constants";
|
||||||
|
|
||||||
import type { MaybePromise, ResolutionType } from "./utility-types";
|
import type { MaybePromise, ResolutionType } from "./utility-types";
|
||||||
|
|
||||||
|
import type { EVENT } from "./constants";
|
||||||
|
|
||||||
let mockDateTime: string | null = null;
|
let mockDateTime: string | null = null;
|
||||||
|
|
||||||
export const setDateTimeForTests = (dateTime: string) => {
|
export const setDateTimeForTests = (dateTime: string) => {
|
||||||
@@ -167,7 +173,7 @@ export const throttleRAF = <T extends any[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ret = (...args: T) => {
|
const ret = (...args: T) => {
|
||||||
if (import.meta.env.MODE === "test") {
|
if (isTestEnv()) {
|
||||||
fn(...args);
|
fn(...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -380,7 +386,7 @@ export const updateActiveTool = (
|
|||||||
type: ToolType;
|
type: ToolType;
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean }) & {
|
) & { locked?: boolean; fromSelection?: boolean }) & {
|
||||||
lastActiveToolBeforeEraser?: ActiveTool | null;
|
lastActiveToolBeforeEraser?: ActiveTool | null;
|
||||||
},
|
},
|
||||||
): AppState["activeTool"] => {
|
): AppState["activeTool"] => {
|
||||||
@@ -402,6 +408,7 @@ export const updateActiveTool = (
|
|||||||
type: data.type,
|
type: data.type,
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: data.locked ?? appState.activeTool.locked,
|
locked: data.locked ?? appState.activeTool.locked,
|
||||||
|
fromSelection: data.fromSelection ?? false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -673,7 +680,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
|||||||
return items.reduce((acc: Map<string, T>, element) => {
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
acc.set(typeof element === "string" ? element : element.id, element);
|
acc.set(typeof element === "string" ? element : element.id, element);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map());
|
}, new Map() as Map<string, T>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||||
@@ -728,9 +735,11 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
|
|||||||
return acc;
|
return acc;
|
||||||
}, [] as Node<T>[]);
|
}, [] as Node<T>[]);
|
||||||
|
|
||||||
export const isTestEnv = () => import.meta.env.MODE === "test";
|
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === "development";
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
|
|
||||||
|
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||||
|
|
||||||
export const isServerEnv = () =>
|
export const isServerEnv = () =>
|
||||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||||
@@ -1184,54 +1193,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// extending the missing types
|
|
||||||
// relying on the [Index, T] to keep a correct order
|
|
||||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
|
||||||
addEventListener: (
|
|
||||||
type: "fulfilled",
|
|
||||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
|
||||||
) => (event: { data: { result: [Index, T] } }) => void;
|
|
||||||
removeEventListener: (
|
|
||||||
type: "fulfilled",
|
|
||||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PromisePool<T> {
|
|
||||||
private readonly pool: TPromisePool<T>;
|
|
||||||
private readonly entries: Record<number, T> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
|
||||||
concurrency: number,
|
|
||||||
) {
|
|
||||||
this.pool = new Pool(
|
|
||||||
source as unknown as () => void | PromiseLike<[number, T][]>,
|
|
||||||
concurrency,
|
|
||||||
) as TPromisePool<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public all() {
|
|
||||||
const listener = (event: { data: { result: void | [number, T] } }) => {
|
|
||||||
if (event.data.result) {
|
|
||||||
// by default pool does not return the results, so we are gathering them manually
|
|
||||||
// with the correct call order (represented by the index in the tuple)
|
|
||||||
const [index, value] = event.data.result;
|
|
||||||
this.entries[index] = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pool.addEventListener("fulfilled", listener);
|
|
||||||
|
|
||||||
return this.pool.start().then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.pool.removeEventListener("fulfilled", listener);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.values(this.entries);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* use when you need to render unsafe string as HTML attribute, but MAKE SURE
|
* use when you need to render unsafe string as HTML attribute, but MAKE SURE
|
||||||
@@ -1243,3 +1204,32 @@ export const escapeDoubleQuotes = (str: string) => {
|
|||||||
|
|
||||||
export const castArray = <T>(value: T | T[]): T[] =>
|
export const castArray = <T>(value: T | T[]): T[] =>
|
||||||
Array.isArray(value) ? value : [value];
|
Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
export const elementCenterPoint = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
xOffset: number = 0,
|
||||||
|
yOffset: number = 0,
|
||||||
|
) => {
|
||||||
|
const { x, y, width, height } = element;
|
||||||
|
|
||||||
|
const centerXPoint = x + width / 2 + xOffset;
|
||||||
|
|
||||||
|
const centerYPoint = y + height / 2 + yOffset;
|
||||||
|
|
||||||
|
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
|
export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
||||||
|
return Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sizeOf = (
|
||||||
|
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
|
||||||
|
): number => {
|
||||||
|
return isReadonlyArray(value)
|
||||||
|
? value.length
|
||||||
|
: value instanceof Map
|
||||||
|
? value.size
|
||||||
|
: Object.keys(value).length;
|
||||||
|
};
|
@@ -1,4 +1,4 @@
|
|||||||
import { KEYS, matchKey } from "./keys";
|
import { KEYS, matchKey } from "../src/keys";
|
||||||
|
|
||||||
describe("key matcher", async () => {
|
describe("key matcher", async () => {
|
||||||
it("should not match unexpected key", async () => {
|
it("should not match unexpected key", async () => {
|
@@ -1,4 +1,4 @@
|
|||||||
import { Queue } from "./queue";
|
import { Queue } from "../src/queue";
|
||||||
|
|
||||||
describe("Queue", () => {
|
describe("Queue", () => {
|
||||||
const calls: any[] = [];
|
const calls: any[] = [];
|
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "../src/url";
|
||||||
|
|
||||||
describe("normalizeLink", () => {
|
describe("normalizeLink", () => {
|
||||||
// NOTE not an extensive XSS test suite, just to check if we're not
|
// NOTE not an extensive XSS test suite, just to check if we're not
|
8
packages/common/tsconfig.json
Normal file
8
packages/common/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/types"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "global.d.ts"],
|
||||||
|
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
|
||||||
|
}
|
38
packages/deltas/package.json
Normal file
38
packages/deltas/package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@excalidraw/deltas",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw utilities for handling deltas",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-deltas"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "5.0.9",
|
||||||
|
"roughjs": "4.6.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildShared.js && yarn gen:types",
|
||||||
|
"pack": "yarn build:umd && yarn pack"
|
||||||
|
}
|
||||||
|
}
|
357
packages/deltas/src/common/delta.ts
Normal file
357
packages/deltas/src/common/delta.ts
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { arrayToObject, assertNever } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the difference between two objects of the same type.
|
||||||
|
*
|
||||||
|
* Both `deleted` and `inserted` partials represent the same set of added, removed or updated properties, where:
|
||||||
|
* - `deleted` is a set of all the deleted values
|
||||||
|
* - `inserted` is a set of all the inserted (added, updated) values
|
||||||
|
*
|
||||||
|
* Keeping it as pure object (without transient state, side-effects, etc.), so we won't have to instantiate it on load.
|
||||||
|
*/
|
||||||
|
export class Delta<T> {
|
||||||
|
private constructor(
|
||||||
|
public readonly deleted: Partial<T>,
|
||||||
|
public readonly inserted: Partial<T>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static create<T>(
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
modifier?: (delta: Partial<T>) => Partial<T>,
|
||||||
|
modifierOptions?: "deleted" | "inserted",
|
||||||
|
) {
|
||||||
|
const modifiedDeleted =
|
||||||
|
modifier && modifierOptions !== "inserted" ? modifier(deleted) : deleted;
|
||||||
|
const modifiedInserted =
|
||||||
|
modifier && modifierOptions !== "deleted" ? modifier(inserted) : inserted;
|
||||||
|
|
||||||
|
return new Delta(modifiedDeleted, modifiedInserted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the delta between two objects.
|
||||||
|
*
|
||||||
|
* @param prevObject - The previous state of the object.
|
||||||
|
* @param nextObject - The next state of the object.
|
||||||
|
*
|
||||||
|
* @returns new delta instance.
|
||||||
|
*/
|
||||||
|
public static calculate<T extends { [key: string]: any }>(
|
||||||
|
prevObject: T,
|
||||||
|
nextObject: T,
|
||||||
|
modifier?: (partial: Partial<T>) => Partial<T>,
|
||||||
|
postProcess?: (
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
) => [Partial<T>, Partial<T>],
|
||||||
|
): Delta<T> {
|
||||||
|
if (prevObject === nextObject) {
|
||||||
|
return Delta.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = {} as Partial<T>;
|
||||||
|
const inserted = {} as Partial<T>;
|
||||||
|
|
||||||
|
// O(n^3) here for elements, but it's not as bad as it looks:
|
||||||
|
// - we do this only on store recordings, not on every frame (not for ephemerals)
|
||||||
|
// - we do this only on previously detected changed elements
|
||||||
|
// - we do shallow compare only on the first level of properties (not going any deeper)
|
||||||
|
// - # of properties is reasonably small
|
||||||
|
for (const key of this.distinctKeysIterator(
|
||||||
|
"full",
|
||||||
|
prevObject,
|
||||||
|
nextObject,
|
||||||
|
)) {
|
||||||
|
deleted[key as keyof T] = prevObject[key];
|
||||||
|
inserted[key as keyof T] = nextObject[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [processedDeleted, processedInserted] = postProcess
|
||||||
|
? postProcess(deleted, inserted)
|
||||||
|
: [deleted, inserted];
|
||||||
|
|
||||||
|
return Delta.create(processedDeleted, processedInserted, modifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static empty() {
|
||||||
|
return new Delta({}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isEmpty<T>(delta: Delta<T>): boolean {
|
||||||
|
return (
|
||||||
|
!Object.keys(delta.deleted).length && !Object.keys(delta.inserted).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges deleted and inserted object partials.
|
||||||
|
*/
|
||||||
|
public static mergeObjects<T extends { [key: string]: unknown }>(
|
||||||
|
prev: T,
|
||||||
|
added: T,
|
||||||
|
removed: T,
|
||||||
|
) {
|
||||||
|
const cloned = { ...prev };
|
||||||
|
|
||||||
|
for (const key of Object.keys(removed)) {
|
||||||
|
delete cloned[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...cloned, ...added };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges deleted and inserted array partials.
|
||||||
|
*/
|
||||||
|
public static mergeArrays<T>(
|
||||||
|
prev: readonly T[] | null,
|
||||||
|
added: readonly T[] | null | undefined,
|
||||||
|
removed: readonly T[] | null | undefined,
|
||||||
|
predicate?: (value: T) => string,
|
||||||
|
) {
|
||||||
|
return Object.values(
|
||||||
|
Delta.mergeObjects(
|
||||||
|
arrayToObject(prev ?? [], predicate),
|
||||||
|
arrayToObject(added ?? [], predicate),
|
||||||
|
arrayToObject(removed ?? [], predicate),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diff object partials as part of the `postProcess`.
|
||||||
|
*/
|
||||||
|
public static diffObjects<T, K extends keyof T, V extends T[K][keyof T[K]]>(
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
property: K,
|
||||||
|
setValue: (prevValue: V | undefined) => V,
|
||||||
|
) {
|
||||||
|
if (!deleted[property] && !inserted[property]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof deleted[property] === "object" ||
|
||||||
|
typeof inserted[property] === "object"
|
||||||
|
) {
|
||||||
|
type RecordLike = Record<string, V | undefined>;
|
||||||
|
|
||||||
|
const deletedObject: RecordLike = deleted[property] ?? {};
|
||||||
|
const insertedObject: RecordLike = inserted[property] ?? {};
|
||||||
|
|
||||||
|
const deletedDifferences = Delta.getLeftDifferences(
|
||||||
|
deletedObject,
|
||||||
|
insertedObject,
|
||||||
|
).reduce((acc, curr) => {
|
||||||
|
acc[curr] = setValue(deletedObject[curr]);
|
||||||
|
return acc;
|
||||||
|
}, {} as RecordLike);
|
||||||
|
|
||||||
|
const insertedDifferences = Delta.getRightDifferences(
|
||||||
|
deletedObject,
|
||||||
|
insertedObject,
|
||||||
|
).reduce((acc, curr) => {
|
||||||
|
acc[curr] = setValue(insertedObject[curr]);
|
||||||
|
return acc;
|
||||||
|
}, {} as RecordLike);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(deletedDifferences).length ||
|
||||||
|
Object.keys(insertedDifferences).length
|
||||||
|
) {
|
||||||
|
Reflect.set(deleted, property, deletedDifferences);
|
||||||
|
Reflect.set(inserted, property, insertedDifferences);
|
||||||
|
} else {
|
||||||
|
Reflect.deleteProperty(deleted, property);
|
||||||
|
Reflect.deleteProperty(inserted, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diff array partials as part of the `postProcess`.
|
||||||
|
*/
|
||||||
|
public static diffArrays<T, K extends keyof T, V extends T[K]>(
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
property: K,
|
||||||
|
groupBy: (value: V extends ArrayLike<infer T> ? T : never) => string,
|
||||||
|
) {
|
||||||
|
if (!deleted[property] && !inserted[property]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(deleted[property]) || Array.isArray(inserted[property])) {
|
||||||
|
const deletedArray = (
|
||||||
|
Array.isArray(deleted[property]) ? deleted[property] : []
|
||||||
|
) as [];
|
||||||
|
const insertedArray = (
|
||||||
|
Array.isArray(inserted[property]) ? inserted[property] : []
|
||||||
|
) as [];
|
||||||
|
|
||||||
|
const deletedDifferences = arrayToObject(
|
||||||
|
Delta.getLeftDifferences(
|
||||||
|
arrayToObject(deletedArray, groupBy),
|
||||||
|
arrayToObject(insertedArray, groupBy),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const insertedDifferences = arrayToObject(
|
||||||
|
Delta.getRightDifferences(
|
||||||
|
arrayToObject(deletedArray, groupBy),
|
||||||
|
arrayToObject(insertedArray, groupBy),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(deletedDifferences).length ||
|
||||||
|
Object.keys(insertedDifferences).length
|
||||||
|
) {
|
||||||
|
const deletedValue = deletedArray.filter(
|
||||||
|
(x) => deletedDifferences[groupBy ? groupBy(x) : String(x)],
|
||||||
|
);
|
||||||
|
const insertedValue = insertedArray.filter(
|
||||||
|
(x) => insertedDifferences[groupBy ? groupBy(x) : String(x)],
|
||||||
|
);
|
||||||
|
|
||||||
|
Reflect.set(deleted, property, deletedValue);
|
||||||
|
Reflect.set(inserted, property, insertedValue);
|
||||||
|
} else {
|
||||||
|
Reflect.deleteProperty(deleted, property);
|
||||||
|
Reflect.deleteProperty(inserted, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares if object1 contains any different value compared to the object2.
|
||||||
|
*/
|
||||||
|
public static isLeftDifferent<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
): boolean {
|
||||||
|
const anyDistinctKey = this.distinctKeysIterator(
|
||||||
|
"left",
|
||||||
|
object1,
|
||||||
|
object2,
|
||||||
|
skipShallowCompare,
|
||||||
|
).next().value;
|
||||||
|
|
||||||
|
return !!anyDistinctKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares if object2 contains any different value compared to the object1.
|
||||||
|
*/
|
||||||
|
public static isRightDifferent<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
): boolean {
|
||||||
|
const anyDistinctKey = this.distinctKeysIterator(
|
||||||
|
"right",
|
||||||
|
object1,
|
||||||
|
object2,
|
||||||
|
skipShallowCompare,
|
||||||
|
).next().value;
|
||||||
|
|
||||||
|
return !!anyDistinctKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the object1 keys that have distinct values.
|
||||||
|
*/
|
||||||
|
public static getLeftDifferences<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
) {
|
||||||
|
return Array.from(
|
||||||
|
this.distinctKeysIterator("left", object1, object2, skipShallowCompare),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the object2 keys that have distinct values.
|
||||||
|
*/
|
||||||
|
public static getRightDifferences<T extends {}>(
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
) {
|
||||||
|
return Array.from(
|
||||||
|
this.distinctKeysIterator("right", object1, object2, skipShallowCompare),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator comparing values of object properties based on the passed joining strategy.
|
||||||
|
*
|
||||||
|
* @yields keys of properties with different values
|
||||||
|
*
|
||||||
|
* WARN: it's based on shallow compare performed only on the first level and doesn't go deeper than that.
|
||||||
|
*/
|
||||||
|
private static *distinctKeysIterator<T extends {}>(
|
||||||
|
join: "left" | "right" | "full",
|
||||||
|
object1: T,
|
||||||
|
object2: T,
|
||||||
|
skipShallowCompare = false,
|
||||||
|
) {
|
||||||
|
if (object1 === object2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys: string[] = [];
|
||||||
|
|
||||||
|
if (join === "left") {
|
||||||
|
keys = Object.keys(object1);
|
||||||
|
} else if (join === "right") {
|
||||||
|
keys = Object.keys(object2);
|
||||||
|
} else if (join === "full") {
|
||||||
|
keys = Array.from(
|
||||||
|
new Set([...Object.keys(object1), ...Object.keys(object2)]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertNever(join, "Unknown distinctKeysIterator's join param");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const object1Value = object1[key as keyof T];
|
||||||
|
const object2Value = object2[key as keyof T];
|
||||||
|
|
||||||
|
if (object1Value !== object2Value) {
|
||||||
|
if (
|
||||||
|
!skipShallowCompare &&
|
||||||
|
typeof object1Value === "object" &&
|
||||||
|
typeof object2Value === "object" &&
|
||||||
|
object1Value !== null &&
|
||||||
|
object2Value !== null &&
|
||||||
|
this.isShallowEqual(object1Value, object2Value)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isShallowEqual(object1: any, object2: any) {
|
||||||
|
const keys1 = Object.keys(object1);
|
||||||
|
const keys2 = Object.keys(object1);
|
||||||
|
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys1) {
|
||||||
|
if (object1[key] !== object2[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
21
packages/deltas/src/common/interfaces.ts
Normal file
21
packages/deltas/src/common/interfaces.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Encapsulates a set of application-level `Delta`s.
|
||||||
|
*/
|
||||||
|
export interface DeltaContainer<T> {
|
||||||
|
/**
|
||||||
|
* Inverses the `Delta`s while creating a new `DeltaContainer` instance.
|
||||||
|
*/
|
||||||
|
inverse(): DeltaContainer<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the `Delta`s to the previous object.
|
||||||
|
*
|
||||||
|
* @returns a tuple of the next object `T` with applied `Delta`s, and `boolean`, indicating whether the applied deltas resulted in a visible change.
|
||||||
|
*/
|
||||||
|
applyTo(previous: T, ...options: unknown[]): [T, boolean];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether all `Delta`s are empty.
|
||||||
|
*/
|
||||||
|
isEmpty(): boolean;
|
||||||
|
}
|
149
packages/deltas/src/common/utils.ts
Normal file
149
packages/deltas/src/common/utils.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { Random } from "roughjs/bin/math";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
ObservedAppState,
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ElementUpdate,
|
||||||
|
} from "../excalidraw-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform array into an object, use only when array order is irrelevant.
|
||||||
|
*/
|
||||||
|
export const arrayToObject = <T>(
|
||||||
|
array: readonly T[],
|
||||||
|
groupBy?: (value: T) => string | number,
|
||||||
|
) =>
|
||||||
|
array.reduce((acc, value) => {
|
||||||
|
acc[groupBy ? groupBy(value) : String(value)] = value;
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: T });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms array of elements with `id` property into into a Map grouped by `id`.
|
||||||
|
*/
|
||||||
|
export const elementsToMap = <T extends { id: string }>(
|
||||||
|
items: readonly T[],
|
||||||
|
) => {
|
||||||
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
|
acc.set(element.id, element);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
};
|
||||||
|
|
||||||
|
// --
|
||||||
|
|
||||||
|
// hidden non-enumerable property for runtime checks
|
||||||
|
const hiddenObservedAppStateProp = "__observedAppState";
|
||||||
|
|
||||||
|
export const getObservedAppState = (appState: AppState): ObservedAppState => {
|
||||||
|
const observedAppState = {
|
||||||
|
name: appState.name,
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
selectedGroupIds: appState.selectedGroupIds,
|
||||||
|
editingLinearElementId: appState.editingLinearElement?.elementId || null,
|
||||||
|
selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
|
||||||
|
croppingElementId: appState.croppingElementId,
|
||||||
|
};
|
||||||
|
|
||||||
|
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
||||||
|
value: true,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return observedAppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
export const assertNever = (value: never, message: string): never => {
|
||||||
|
throw new Error(`${message}: "${value}".`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getNonDeletedGroupIds = (elements: ElementsMap) => {
|
||||||
|
const nonDeletedGroupIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const [, element] of elements) {
|
||||||
|
// defensive check
|
||||||
|
if (element.isDeleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// defensive fallback
|
||||||
|
for (const groupId of element.groupIds ?? []) {
|
||||||
|
nonDeletedGroupIds.add(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonDeletedGroupIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
export const isTestEnv = () => import.meta.env.MODE === "test";
|
||||||
|
|
||||||
|
export const isDevEnv = () => import.meta.env.MODE === "development";
|
||||||
|
|
||||||
|
export const isServerEnv = () => import.meta.env.MODE === "server";
|
||||||
|
|
||||||
|
export const shouldThrow = () => isDevEnv() || isTestEnv() || isServerEnv();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
let random = new Random(Date.now());
|
||||||
|
let testIdBase = 0;
|
||||||
|
|
||||||
|
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||||
|
|
||||||
|
export const reseed = (seed: number) => {
|
||||||
|
random = new Random(seed);
|
||||||
|
testIdBase = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const randomId = () => (isTestEnv() ? `id${testIdBase++}` : nanoid());
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
/** pass `true` to always regenerate */
|
||||||
|
force = false,
|
||||||
|
): TElement => {
|
||||||
|
let didChange = false;
|
||||||
|
for (const key in updates) {
|
||||||
|
const value = (updates as any)[key];
|
||||||
|
if (typeof value !== "undefined") {
|
||||||
|
if (
|
||||||
|
(element as any)[key] === value &&
|
||||||
|
// if object, always update because its attrs could have changed
|
||||||
|
(typeof value !== "object" || value === null)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
didChange = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!didChange && !force) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...element,
|
||||||
|
...updates,
|
||||||
|
updated: getUpdatedTimestamp(),
|
||||||
|
version: element.version + 1,
|
||||||
|
versionNonce: randomInteger(),
|
||||||
|
};
|
||||||
|
};
|
404
packages/deltas/src/containers/appstate.ts
Normal file
404
packages/deltas/src/containers/appstate.ts
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
import { Delta } from "../common/delta";
|
||||||
|
import {
|
||||||
|
assertNever,
|
||||||
|
getNonDeletedGroupIds,
|
||||||
|
getObservedAppState,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
|
shouldThrow,
|
||||||
|
} from "../common/utils";
|
||||||
|
|
||||||
|
import type { DeltaContainer } from "../common/interfaces";
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
ObservedAppState,
|
||||||
|
DTO,
|
||||||
|
SceneElementsMap,
|
||||||
|
ValueOf,
|
||||||
|
ObservedElementsAppState,
|
||||||
|
ObservedStandaloneAppState,
|
||||||
|
SubtypeOf,
|
||||||
|
} from "../excalidraw-types";
|
||||||
|
|
||||||
|
export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
|
private constructor(public readonly delta: Delta<ObservedAppState>) {}
|
||||||
|
|
||||||
|
public static calculate<T extends ObservedAppState>(
|
||||||
|
prevAppState: T,
|
||||||
|
nextAppState: T,
|
||||||
|
): AppStateDelta {
|
||||||
|
const delta = Delta.calculate(
|
||||||
|
prevAppState,
|
||||||
|
nextAppState,
|
||||||
|
undefined,
|
||||||
|
AppStateDelta.postProcess,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AppStateDelta(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static restore(appStateDeltaDTO: DTO<AppStateDelta>): AppStateDelta {
|
||||||
|
const { delta } = appStateDeltaDTO;
|
||||||
|
return new AppStateDelta(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static empty() {
|
||||||
|
return new AppStateDelta(Delta.create({}, {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public inverse(): AppStateDelta {
|
||||||
|
const inversedDelta = Delta.create(this.delta.inserted, this.delta.deleted);
|
||||||
|
return new AppStateDelta(inversedDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public applyTo(
|
||||||
|
appState: AppState,
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
): [AppState, boolean] {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
selectedElementIds: removedSelectedElementIds = {},
|
||||||
|
selectedGroupIds: removedSelectedGroupIds = {},
|
||||||
|
} = this.delta.deleted;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedElementIds: addedSelectedElementIds = {},
|
||||||
|
selectedGroupIds: addedSelectedGroupIds = {},
|
||||||
|
selectedLinearElementId,
|
||||||
|
editingLinearElementId,
|
||||||
|
...directlyApplicablePartial
|
||||||
|
} = this.delta.inserted;
|
||||||
|
|
||||||
|
const mergedSelectedElementIds = Delta.mergeObjects(
|
||||||
|
appState.selectedElementIds,
|
||||||
|
addedSelectedElementIds,
|
||||||
|
removedSelectedElementIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedSelectedGroupIds = Delta.mergeObjects(
|
||||||
|
appState.selectedGroupIds,
|
||||||
|
addedSelectedGroupIds,
|
||||||
|
removedSelectedGroupIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
// const selectedLinearElement =
|
||||||
|
// selectedLinearElementId && nextElements.has(selectedLinearElementId)
|
||||||
|
// ? new LinearElementEditor(
|
||||||
|
// nextElements.get(
|
||||||
|
// selectedLinearElementId,
|
||||||
|
// ) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
// )
|
||||||
|
// : null;
|
||||||
|
|
||||||
|
// const editingLinearElement =
|
||||||
|
// editingLinearElementId && nextElements.has(editingLinearElementId)
|
||||||
|
// ? new LinearElementEditor(
|
||||||
|
// nextElements.get(
|
||||||
|
// editingLinearElementId,
|
||||||
|
// ) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
// )
|
||||||
|
// : null;
|
||||||
|
|
||||||
|
const nextAppState = {
|
||||||
|
...appState,
|
||||||
|
...directlyApplicablePartial,
|
||||||
|
selectedElementIds: mergedSelectedElementIds,
|
||||||
|
selectedGroupIds: mergedSelectedGroupIds,
|
||||||
|
// selectedLinearElement:
|
||||||
|
// typeof selectedLinearElementId !== "undefined"
|
||||||
|
// ? selectedLinearElement // element was either inserted or deleted
|
||||||
|
// : appState.selectedLinearElement, // otherwise assign what we had before
|
||||||
|
// editingLinearElement:
|
||||||
|
// typeof editingLinearElementId !== "undefined"
|
||||||
|
// ? editingLinearElement // element was either inserted or deleted
|
||||||
|
// : appState.editingLinearElement, // otherwise assign what we had before
|
||||||
|
};
|
||||||
|
|
||||||
|
const constainsVisibleChanges = this.filterInvisibleChanges(
|
||||||
|
appState,
|
||||||
|
nextAppState,
|
||||||
|
nextElements,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [nextAppState, constainsVisibleChanges];
|
||||||
|
} catch (e) {
|
||||||
|
// shouldn't really happen, but just in case
|
||||||
|
console.error(`Couldn't apply appstate delta`, e);
|
||||||
|
|
||||||
|
if (shouldThrow()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [appState, false];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return Delta.isEmpty(this.delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is necessary to post process the partials in case of reference values,
|
||||||
|
* for which we need to calculate the real diff between `deleted` and `inserted`.
|
||||||
|
*/
|
||||||
|
private static postProcess<T extends ObservedAppState>(
|
||||||
|
deleted: Partial<T>,
|
||||||
|
inserted: Partial<T>,
|
||||||
|
): [Partial<T>, Partial<T>] {
|
||||||
|
try {
|
||||||
|
Delta.diffObjects(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
"selectedElementIds",
|
||||||
|
// ts language server has a bit trouble resolving this, so we are giving it a little push
|
||||||
|
(_) => true as ValueOf<T["selectedElementIds"]>,
|
||||||
|
);
|
||||||
|
Delta.diffObjects(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
"selectedGroupIds",
|
||||||
|
(prevValue) => (prevValue ?? false) as ValueOf<T["selectedGroupIds"]>,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
||||||
|
console.error(`Couldn't postprocess appstate change deltas.`);
|
||||||
|
|
||||||
|
if (isDevEnv() || isTestEnv()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
return [deleted, inserted];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutates `nextAppState` be filtering out state related to deleted elements.
|
||||||
|
*
|
||||||
|
* @returns `true` if a visible change is found, `false` otherwise.
|
||||||
|
*/
|
||||||
|
private filterInvisibleChanges(
|
||||||
|
prevAppState: AppState,
|
||||||
|
nextAppState: AppState,
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
): boolean {
|
||||||
|
// TODO: #7348 we could still get an empty undo/redo, as we assume that previous appstate does not contain references to deleted elements
|
||||||
|
// which is not always true - i.e. now we do cleanup appstate during history, but we do not do it during remote updates
|
||||||
|
const prevObservedAppState = getObservedAppState(prevAppState);
|
||||||
|
const nextObservedAppState = getObservedAppState(nextAppState);
|
||||||
|
|
||||||
|
const containsStandaloneDifference = Delta.isRightDifferent(
|
||||||
|
AppStateDelta.stripElementsProps(prevObservedAppState),
|
||||||
|
AppStateDelta.stripElementsProps(nextObservedAppState),
|
||||||
|
);
|
||||||
|
|
||||||
|
const containsElementsDifference = Delta.isRightDifferent(
|
||||||
|
AppStateDelta.stripStandaloneProps(prevObservedAppState),
|
||||||
|
AppStateDelta.stripStandaloneProps(nextObservedAppState),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!containsStandaloneDifference && !containsElementsDifference) {
|
||||||
|
// no change in appstate was detected
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleDifferenceFlag = {
|
||||||
|
value: containsStandaloneDifference,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (containsElementsDifference) {
|
||||||
|
// filter invisible changes on each iteration
|
||||||
|
const changedElementsProps = Delta.getRightDifferences(
|
||||||
|
AppStateDelta.stripStandaloneProps(prevObservedAppState),
|
||||||
|
AppStateDelta.stripStandaloneProps(nextObservedAppState),
|
||||||
|
) as Array<keyof ObservedElementsAppState>;
|
||||||
|
|
||||||
|
let nonDeletedGroupIds = new Set<string>();
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedElementsProps.includes("editingGroupId") ||
|
||||||
|
changedElementsProps.includes("selectedGroupIds")
|
||||||
|
) {
|
||||||
|
// this one iterates through all the non deleted elements, so make sure it's not done twice
|
||||||
|
nonDeletedGroupIds = getNonDeletedGroupIds(nextElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether delta properties are related to the existing non-deleted elements
|
||||||
|
for (const key of changedElementsProps) {
|
||||||
|
switch (key) {
|
||||||
|
case "selectedElementIds":
|
||||||
|
nextAppState[key] = AppStateDelta.filterSelectedElements(
|
||||||
|
nextAppState[key],
|
||||||
|
nextElements,
|
||||||
|
visibleDifferenceFlag,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "selectedGroupIds":
|
||||||
|
nextAppState[key] = AppStateDelta.filterSelectedGroups(
|
||||||
|
nextAppState[key],
|
||||||
|
nonDeletedGroupIds,
|
||||||
|
visibleDifferenceFlag,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "croppingElementId": {
|
||||||
|
const croppingElementId = nextAppState[key];
|
||||||
|
const element =
|
||||||
|
croppingElementId && nextElements.get(croppingElementId);
|
||||||
|
|
||||||
|
if (element && !element.isDeleted) {
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
nextAppState[key] = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "editingGroupId":
|
||||||
|
const editingGroupId = nextAppState[key];
|
||||||
|
|
||||||
|
if (!editingGroupId) {
|
||||||
|
// previously there was an editingGroup (assuming visible), now there is none
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else if (nonDeletedGroupIds.has(editingGroupId)) {
|
||||||
|
// previously there wasn't an editingGroup, now there is one which is visible
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
// there was assigned an editingGroup now, but it's related to deleted element
|
||||||
|
nextAppState[key] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "selectedLinearElementId":
|
||||||
|
case "editingLinearElementId":
|
||||||
|
const appStateKey = AppStateDelta.convertToAppStateKey(key);
|
||||||
|
const linearElement = nextAppState[appStateKey];
|
||||||
|
|
||||||
|
if (!linearElement) {
|
||||||
|
// previously there was a linear element (assuming visible), now there is none
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
const element = nextElements.get(linearElement.elementId);
|
||||||
|
|
||||||
|
if (element && !element.isDeleted) {
|
||||||
|
// previously there wasn't a linear element, now there is one which is visible
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
// there was assigned a linear element now, but it's deleted
|
||||||
|
nextAppState[appStateKey] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
assertNever(key, `Unknown ObservedElementsAppState's key "${key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleDifferenceFlag.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static convertToAppStateKey(
|
||||||
|
key: keyof Pick<
|
||||||
|
ObservedElementsAppState,
|
||||||
|
"selectedLinearElementId" | "editingLinearElementId"
|
||||||
|
>,
|
||||||
|
): keyof Pick<AppState, "selectedLinearElement" | "editingLinearElement"> {
|
||||||
|
switch (key) {
|
||||||
|
case "selectedLinearElementId":
|
||||||
|
return "selectedLinearElement";
|
||||||
|
case "editingLinearElementId":
|
||||||
|
return "editingLinearElement";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static filterSelectedElements(
|
||||||
|
selectedElementIds: AppState["selectedElementIds"],
|
||||||
|
elements: SceneElementsMap,
|
||||||
|
visibleDifferenceFlag: { value: boolean },
|
||||||
|
) {
|
||||||
|
const ids = Object.keys(selectedElementIds);
|
||||||
|
|
||||||
|
if (!ids.length) {
|
||||||
|
// previously there were ids (assuming related to visible elements), now there are none
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
return selectedElementIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSelectedElementIds = { ...selectedElementIds };
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const element = elements.get(id);
|
||||||
|
|
||||||
|
if (element && !element.isDeleted) {
|
||||||
|
// there is a selected element id related to a visible element
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
delete nextSelectedElementIds[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextSelectedElementIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static filterSelectedGroups(
|
||||||
|
selectedGroupIds: AppState["selectedGroupIds"],
|
||||||
|
nonDeletedGroupIds: Set<string>,
|
||||||
|
visibleDifferenceFlag: { value: boolean },
|
||||||
|
) {
|
||||||
|
const ids = Object.keys(selectedGroupIds);
|
||||||
|
|
||||||
|
if (!ids.length) {
|
||||||
|
// previously there were ids (assuming related to visible groups), now there are none
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
return selectedGroupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSelectedGroupIds = { ...selectedGroupIds };
|
||||||
|
|
||||||
|
for (const id of Object.keys(nextSelectedGroupIds)) {
|
||||||
|
if (nonDeletedGroupIds.has(id)) {
|
||||||
|
// there is a selected group id related to a visible group
|
||||||
|
visibleDifferenceFlag.value = true;
|
||||||
|
} else {
|
||||||
|
delete nextSelectedGroupIds[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextSelectedGroupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static stripElementsProps(
|
||||||
|
delta: Partial<ObservedAppState>,
|
||||||
|
): Partial<ObservedStandaloneAppState> {
|
||||||
|
// WARN: Do not remove the type-casts as they here to ensure proper type checks
|
||||||
|
const {
|
||||||
|
editingGroupId,
|
||||||
|
selectedGroupIds,
|
||||||
|
selectedElementIds,
|
||||||
|
editingLinearElementId,
|
||||||
|
selectedLinearElementId,
|
||||||
|
croppingElementId,
|
||||||
|
...standaloneProps
|
||||||
|
} = delta as ObservedAppState;
|
||||||
|
|
||||||
|
return standaloneProps as SubtypeOf<
|
||||||
|
typeof standaloneProps,
|
||||||
|
ObservedStandaloneAppState
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static stripStandaloneProps(
|
||||||
|
delta: Partial<ObservedAppState>,
|
||||||
|
): Partial<ObservedElementsAppState> {
|
||||||
|
// WARN: Do not remove the type-casts as they here to ensure proper type checks
|
||||||
|
const { name, viewBackgroundColor, ...elementsProps } =
|
||||||
|
delta as ObservedAppState;
|
||||||
|
|
||||||
|
return elementsProps as SubtypeOf<
|
||||||
|
typeof elementsProps,
|
||||||
|
ObservedElementsAppState
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
}
|
825
packages/deltas/src/containers/elements.ts
Normal file
825
packages/deltas/src/containers/elements.ts
Normal file
@@ -0,0 +1,825 @@
|
|||||||
|
import { Delta } from "../common/delta";
|
||||||
|
import { newElementWith, shouldThrow } from "../common/utils";
|
||||||
|
|
||||||
|
import type { DeltaContainer } from "../common/interfaces";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ElementUpdate,
|
||||||
|
Ordered,
|
||||||
|
SceneElementsMap,
|
||||||
|
DTO,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
|
} from "../excalidraw-types";
|
||||||
|
|
||||||
|
// CFDO: consider adding here (nonnullable) version & versionNonce & updated (so that we have correct versions when recunstructing from remote)
|
||||||
|
type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> =
|
||||||
|
ElementUpdate<Ordered<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elements delta is a low level primitive to encapsulate property changes between two sets of elements.
|
||||||
|
* It does so by encapsulating forward and backward `Delta`s, allowing to time-travel in both directions.
|
||||||
|
*/
|
||||||
|
export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||||
|
private constructor(
|
||||||
|
public readonly added: Record<string, Delta<ElementPartial>>,
|
||||||
|
public readonly removed: Record<string, Delta<ElementPartial>>,
|
||||||
|
public readonly updated: Record<string, Delta<ElementPartial>>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
added: Record<string, Delta<ElementPartial>>,
|
||||||
|
removed: Record<string, Delta<ElementPartial>>,
|
||||||
|
updated: Record<string, Delta<ElementPartial>>,
|
||||||
|
options: {
|
||||||
|
shouldRedistribute: boolean;
|
||||||
|
} = {
|
||||||
|
shouldRedistribute: false,
|
||||||
|
// CFDO: don't forget to re-enable
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { shouldRedistribute } = options;
|
||||||
|
let delta: ElementsDelta;
|
||||||
|
|
||||||
|
if (shouldRedistribute) {
|
||||||
|
const nextAdded: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
const nextRemoved: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
const nextUpdated: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
|
const deltas = [
|
||||||
|
...Object.entries(added),
|
||||||
|
...Object.entries(removed),
|
||||||
|
...Object.entries(updated),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [id, delta] of deltas) {
|
||||||
|
if (this.satisfiesAddition(delta)) {
|
||||||
|
nextAdded[id] = delta;
|
||||||
|
} else if (this.satisfiesRemoval(delta)) {
|
||||||
|
nextRemoved[id] = delta;
|
||||||
|
} else {
|
||||||
|
nextUpdated[id] = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delta = new ElementsDelta(nextAdded, nextRemoved, nextUpdated);
|
||||||
|
} else {
|
||||||
|
delta = new ElementsDelta(added, removed, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldThrow()) {
|
||||||
|
ElementsDelta.validate(delta, "added", this.satisfiesAddition);
|
||||||
|
ElementsDelta.validate(delta, "removed", this.satisfiesRemoval);
|
||||||
|
ElementsDelta.validate(delta, "updated", this.satisfiesUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static restore(elementsDeltaDTO: DTO<ElementsDelta>): ElementsDelta {
|
||||||
|
const { added, removed, updated } = elementsDeltaDTO;
|
||||||
|
return ElementsDelta.create(added, removed, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static satisfiesAddition = ({
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
}: Delta<ElementPartial>) =>
|
||||||
|
// dissallowing added as "deleted", which could cause issues when resolving conflicts
|
||||||
|
deleted.isDeleted === true && !inserted.isDeleted;
|
||||||
|
|
||||||
|
private static satisfiesRemoval = ({
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
}: Delta<ElementPartial>) =>
|
||||||
|
!deleted.isDeleted && inserted.isDeleted === true;
|
||||||
|
|
||||||
|
private static satisfiesUpdate = ({
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
}: Delta<ElementPartial>) => !!deleted.isDeleted === !!inserted.isDeleted;
|
||||||
|
|
||||||
|
private static validate(
|
||||||
|
elementsDelta: ElementsDelta,
|
||||||
|
type: "added" | "removed" | "updated",
|
||||||
|
satifies: (delta: Delta<ElementPartial>) => boolean,
|
||||||
|
) {
|
||||||
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
||||||
|
if (!satifies(delta)) {
|
||||||
|
console.error(
|
||||||
|
`Broken invariant for "${type}" delta, element "${id}", delta:`,
|
||||||
|
delta,
|
||||||
|
);
|
||||||
|
throw new Error(`ElementsDelta invariant broken for element "${id}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the `Delta`s between the previous and next set of elements.
|
||||||
|
*
|
||||||
|
* @param prevElements - Map representing the previous state of elements.
|
||||||
|
* @param nextElements - Map representing the next state of elements.
|
||||||
|
*
|
||||||
|
* @returns `ElementsDelta` instance representing the `Delta` changes between the two sets of elements.
|
||||||
|
*/
|
||||||
|
public static calculate<T extends OrderedExcalidrawElement>(
|
||||||
|
prevElements: Map<string, T>,
|
||||||
|
nextElements: Map<string, T>,
|
||||||
|
): ElementsDelta {
|
||||||
|
if (prevElements === nextElements) {
|
||||||
|
return ElementsDelta.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const added: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
const removed: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
const updated: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
|
// this might be needed only in same edge cases, like during collab, when `isDeleted` elements get removed or when we (un)intentionally remove the elements
|
||||||
|
for (const prevElement of prevElements.values()) {
|
||||||
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
|
if (!nextElement) {
|
||||||
|
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
||||||
|
const inserted = { isDeleted: true } as ElementPartial;
|
||||||
|
|
||||||
|
const delta = Delta.create(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
ElementsDelta.stripIrrelevantProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
removed[prevElement.id] = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const nextElement of nextElements.values()) {
|
||||||
|
const prevElement = prevElements.get(nextElement.id);
|
||||||
|
|
||||||
|
if (!prevElement) {
|
||||||
|
const deleted = { isDeleted: true } as ElementPartial;
|
||||||
|
const inserted = {
|
||||||
|
...nextElement,
|
||||||
|
isDeleted: false,
|
||||||
|
} as ElementPartial;
|
||||||
|
|
||||||
|
const delta = Delta.create(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
ElementsDelta.stripIrrelevantProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
added[nextElement.id] = delta;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevElement.versionNonce !== nextElement.versionNonce) {
|
||||||
|
const delta = Delta.calculate<ElementPartial>(
|
||||||
|
prevElement,
|
||||||
|
nextElement,
|
||||||
|
ElementsDelta.stripIrrelevantProps,
|
||||||
|
ElementsDelta.postProcess,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// making sure we don't get here some non-boolean values (i.e. undefined, null, etc.)
|
||||||
|
typeof prevElement.isDeleted === "boolean" &&
|
||||||
|
typeof nextElement.isDeleted === "boolean" &&
|
||||||
|
prevElement.isDeleted !== nextElement.isDeleted
|
||||||
|
) {
|
||||||
|
// notice that other props could have been updated as well
|
||||||
|
if (prevElement.isDeleted && !nextElement.isDeleted) {
|
||||||
|
added[nextElement.id] = delta;
|
||||||
|
} else {
|
||||||
|
removed[nextElement.id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// making sure there are at least some changes
|
||||||
|
if (!Delta.isEmpty(delta)) {
|
||||||
|
updated[nextElement.id] = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElementsDelta.create(added, removed, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static empty() {
|
||||||
|
return ElementsDelta.create({}, {}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public inverse(): ElementsDelta {
|
||||||
|
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
|
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
|
for (const [id, delta] of Object.entries(deltas)) {
|
||||||
|
inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inversedDeltas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const added = inverseInternal(this.added);
|
||||||
|
const removed = inverseInternal(this.removed);
|
||||||
|
const updated = inverseInternal(this.updated);
|
||||||
|
|
||||||
|
// notice we inverse removed with added not to break the invariants
|
||||||
|
// notice we force generate a new id
|
||||||
|
return ElementsDelta.create(removed, added, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return (
|
||||||
|
Object.keys(this.added).length === 0 &&
|
||||||
|
Object.keys(this.removed).length === 0 &&
|
||||||
|
Object.keys(this.updated).length === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update delta/s based on the existing elements.
|
||||||
|
*
|
||||||
|
* @param elements current elements
|
||||||
|
* @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated
|
||||||
|
* @returns new instance with modified delta/s
|
||||||
|
*/
|
||||||
|
public applyLatestChanges(
|
||||||
|
elements: SceneElementsMap,
|
||||||
|
modifierOptions: "deleted" | "inserted",
|
||||||
|
): ElementsDelta {
|
||||||
|
const modifier =
|
||||||
|
(element: OrderedExcalidrawElement) => (partial: ElementPartial) => {
|
||||||
|
const latestPartial: { [key: string]: unknown } = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
|
||||||
|
// do not update following props:
|
||||||
|
// - `boundElements`, as it is a reference value which is postprocessed to contain only deleted/inserted keys
|
||||||
|
switch (key) {
|
||||||
|
case "boundElements":
|
||||||
|
latestPartial[key] = partial[key];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
latestPartial[key] = element[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestPartial;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyLatestChangesInternal = (
|
||||||
|
deltas: Record<string, Delta<ElementPartial>>,
|
||||||
|
) => {
|
||||||
|
const modifiedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
|
for (const [id, delta] of Object.entries(deltas)) {
|
||||||
|
const existingElement = elements.get(id);
|
||||||
|
|
||||||
|
if (existingElement) {
|
||||||
|
const modifiedDelta = Delta.create(
|
||||||
|
delta.deleted,
|
||||||
|
delta.inserted,
|
||||||
|
modifier(existingElement),
|
||||||
|
modifierOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
modifiedDeltas[id] = modifiedDelta;
|
||||||
|
} else {
|
||||||
|
modifiedDeltas[id] = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedDeltas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const added = applyLatestChangesInternal(this.added);
|
||||||
|
const removed = applyLatestChangesInternal(this.removed);
|
||||||
|
const updated = applyLatestChangesInternal(this.updated);
|
||||||
|
|
||||||
|
return ElementsDelta.create(added, removed, updated, {
|
||||||
|
shouldRedistribute: true, // redistribute the deltas as `isDeleted` could have been updated
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFDO: does it make sense having a separate snapshot?
|
||||||
|
public applyTo(
|
||||||
|
elements: SceneElementsMap,
|
||||||
|
elementsSnapshot: Map<string, OrderedExcalidrawElement>,
|
||||||
|
): [SceneElementsMap, boolean] {
|
||||||
|
const nextElements = new Map(elements) as SceneElementsMap;
|
||||||
|
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||||
|
|
||||||
|
const flags = {
|
||||||
|
containsVisibleDifference: false,
|
||||||
|
containsZindexDifference: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
||||||
|
try {
|
||||||
|
const applyDeltas = ElementsDelta.createApplier(
|
||||||
|
nextElements,
|
||||||
|
elementsSnapshot,
|
||||||
|
flags,
|
||||||
|
);
|
||||||
|
|
||||||
|
const addedElements = applyDeltas("added", this.added);
|
||||||
|
const removedElements = applyDeltas("removed", this.removed);
|
||||||
|
const updatedElements = applyDeltas("updated", this.updated);
|
||||||
|
|
||||||
|
// CFDO I: don't forget to fix this part
|
||||||
|
// const affectedElements = this.resolveConflicts(elements, nextElements);
|
||||||
|
|
||||||
|
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
||||||
|
changedElements = new Map([
|
||||||
|
...addedElements,
|
||||||
|
...removedElements,
|
||||||
|
...updatedElements,
|
||||||
|
// ...affectedElements,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Couldn't apply elements delta`, e);
|
||||||
|
|
||||||
|
if (shouldThrow()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not really happen, but just in case we cannot apply deltas, let's return the previous elements with visible change set to `true`
|
||||||
|
// even though there is obviously no visible change, returning `false` could be dangerous, as i.e.:
|
||||||
|
// in the worst case, it could lead into iterating through the whole stack with no possibility to redo
|
||||||
|
// instead, the worst case when returning `true` is an empty undo / redo
|
||||||
|
return [elements, true];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CFDO I: don't forget to fix this part
|
||||||
|
// // TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
||||||
|
// ElementsDelta.redrawTextBoundingBoxes(nextElements, changedElements);
|
||||||
|
// // the following reorder performs also mutations, but only on new instances of changed elements
|
||||||
|
// // (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||||
|
// nextElements = ElementsDelta.reorderElements(
|
||||||
|
// nextElements,
|
||||||
|
// changedElements,
|
||||||
|
// flags,
|
||||||
|
// );
|
||||||
|
// // Need ordered nextElements to avoid z-index binding issues
|
||||||
|
// ElementsDelta.redrawBoundArrows(nextElements, changedElements);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Couldn't mutate elements after applying elements change`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldThrow()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
return [nextElements, flags.containsVisibleDifference];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createApplier =
|
||||||
|
(
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
snapshot: Map<string, OrderedExcalidrawElement>,
|
||||||
|
flags: {
|
||||||
|
containsVisibleDifference: boolean;
|
||||||
|
containsZindexDifference: boolean;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
(
|
||||||
|
type: "added" | "removed" | "updated",
|
||||||
|
deltas: Record<string, Delta<ElementPartial>>,
|
||||||
|
) => {
|
||||||
|
const getElement = ElementsDelta.createGetter(
|
||||||
|
type,
|
||||||
|
nextElements,
|
||||||
|
snapshot,
|
||||||
|
flags,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.entries(deltas).reduce((acc, [id, delta]) => {
|
||||||
|
const element = getElement(id, delta.inserted);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const newElement = ElementsDelta.applyDelta(element, delta, flags);
|
||||||
|
nextElements.set(newElement.id, newElement);
|
||||||
|
acc.set(newElement.id, newElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, OrderedExcalidrawElement>());
|
||||||
|
};
|
||||||
|
|
||||||
|
private static createGetter =
|
||||||
|
(
|
||||||
|
type: "added" | "removed" | "updated",
|
||||||
|
elements: SceneElementsMap,
|
||||||
|
snapshot: Map<string, OrderedExcalidrawElement>,
|
||||||
|
flags: {
|
||||||
|
containsVisibleDifference: boolean;
|
||||||
|
containsZindexDifference: boolean;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
(id: string, partial: ElementPartial) => {
|
||||||
|
let element = elements.get(id);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
// always fallback to the local snapshot, in cases when we cannot find the element in the elements array
|
||||||
|
element = snapshot.get(id);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
// as the element was brought from the snapshot, it automatically results in a possible zindex difference
|
||||||
|
flags.containsZindexDifference = true;
|
||||||
|
|
||||||
|
// as the element was force deleted, we need to check if adding it back results in a visible change
|
||||||
|
if (
|
||||||
|
partial.isDeleted === false ||
|
||||||
|
(partial.isDeleted !== true && element.isDeleted === false)
|
||||||
|
) {
|
||||||
|
flags.containsVisibleDifference = true;
|
||||||
|
}
|
||||||
|
} else if (type === "added") {
|
||||||
|
// for additions the element does not have to exist (i.e. remote update)
|
||||||
|
// CFDO II: the version itself might be different!
|
||||||
|
element = newElementWith(
|
||||||
|
{ id, version: 1 } as OrderedExcalidrawElement,
|
||||||
|
{
|
||||||
|
...partial,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static applyDelta(
|
||||||
|
element: OrderedExcalidrawElement,
|
||||||
|
delta: Delta<ElementPartial>,
|
||||||
|
flags: {
|
||||||
|
containsVisibleDifference: boolean;
|
||||||
|
containsZindexDifference: boolean;
|
||||||
|
} = {
|
||||||
|
// by default we don't care about about the flags
|
||||||
|
containsVisibleDifference: true,
|
||||||
|
containsZindexDifference: true,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { boundElements, ...directlyApplicablePartial } = delta.inserted;
|
||||||
|
|
||||||
|
if (
|
||||||
|
delta.deleted.boundElements?.length ||
|
||||||
|
delta.inserted.boundElements?.length
|
||||||
|
) {
|
||||||
|
const mergedBoundElements = Delta.mergeArrays(
|
||||||
|
element.boundElements,
|
||||||
|
delta.inserted.boundElements,
|
||||||
|
delta.deleted.boundElements,
|
||||||
|
(x) => x.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(directlyApplicablePartial, {
|
||||||
|
boundElements: mergedBoundElements,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFDO: this looks wrong
|
||||||
|
if (element.type === "image") {
|
||||||
|
const _delta = delta as Delta<ElementPartial<ExcalidrawImageElement>>;
|
||||||
|
// we want to override `crop` only if modified so that we don't reset
|
||||||
|
// when undoing/redoing unrelated change
|
||||||
|
if (_delta.deleted.crop || _delta.inserted.crop) {
|
||||||
|
Object.assign(directlyApplicablePartial, {
|
||||||
|
// apply change verbatim
|
||||||
|
crop: _delta.inserted.crop ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.containsVisibleDifference) {
|
||||||
|
// strip away fractional index, as even if it would be different, it doesn't have to result in visible change
|
||||||
|
const { index, ...rest } = directlyApplicablePartial;
|
||||||
|
const containsVisibleDifference = ElementsDelta.checkForVisibleDifference(
|
||||||
|
element,
|
||||||
|
rest,
|
||||||
|
);
|
||||||
|
|
||||||
|
flags.containsVisibleDifference = containsVisibleDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.containsZindexDifference) {
|
||||||
|
flags.containsZindexDifference =
|
||||||
|
delta.deleted.index !== delta.inserted.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(element, directlyApplicablePartial);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for visible changes regardless of whether they were removed, added or updated.
|
||||||
|
*/
|
||||||
|
private static checkForVisibleDifference(
|
||||||
|
element: OrderedExcalidrawElement,
|
||||||
|
partial: ElementPartial,
|
||||||
|
) {
|
||||||
|
if (element.isDeleted && partial.isDeleted !== false) {
|
||||||
|
// when it's deleted and partial is not false, it cannot end up with a visible change
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.isDeleted && partial.isDeleted === false) {
|
||||||
|
// when we add an element, it results in a visible change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.isDeleted === false && partial.isDeleted) {
|
||||||
|
// when we remove an element, it results in a visible change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for any difference on a visible element
|
||||||
|
return Delta.isRightDifferent(element, partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Resolves conflicts for all previously added, removed and updated elements.
|
||||||
|
// * Updates the previous deltas with all the changes after conflict resolution.
|
||||||
|
// *
|
||||||
|
// * // CFDO: revisit since arrow seem often redrawn incorrectly
|
||||||
|
// *
|
||||||
|
// * @returns all elements affected by the conflict resolution
|
||||||
|
// */
|
||||||
|
// private resolveConflicts(
|
||||||
|
// prevElements: SceneElementsMap,
|
||||||
|
// nextElements: SceneElementsMap,
|
||||||
|
// ) {
|
||||||
|
// const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
|
// const updater = (
|
||||||
|
// element: ExcalidrawElement,
|
||||||
|
// updates: ElementUpdate<ExcalidrawElement>,
|
||||||
|
// ) => {
|
||||||
|
// const nextElement = nextElements.get(element.id); // only ever modify next element!
|
||||||
|
// if (!nextElement) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let affectedElement: OrderedExcalidrawElement;
|
||||||
|
|
||||||
|
// if (prevElements.get(element.id) === nextElement) {
|
||||||
|
// // create the new element instance in case we didn't modify the element yet
|
||||||
|
// // so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
||||||
|
// affectedElement = newElementWith(
|
||||||
|
// nextElement,
|
||||||
|
// updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// affectedElement = mutateElement(
|
||||||
|
// nextElement,
|
||||||
|
// updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||||
|
// nextElements.set(affectedElement.id, affectedElement);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // removed delta is affecting the bindings always, as all the affected elements of the removed elements need to be unbound
|
||||||
|
// for (const id of Object.keys(this.removed)) {
|
||||||
|
// ElementsDelta.unbindAffected(prevElements, nextElements, id, updater);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // added delta is affecting the bindings always, all the affected elements of the added elements need to be rebound
|
||||||
|
// for (const id of Object.keys(this.added)) {
|
||||||
|
// ElementsDelta.rebindAffected(prevElements, nextElements, id, updater);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // updated delta is affecting the binding only in case it contains changed binding or bindable property
|
||||||
|
// for (const [id] of Array.from(Object.entries(this.updated)).filter(
|
||||||
|
// ([_, delta]) =>
|
||||||
|
// Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) =>
|
||||||
|
// bindingProperties.has(prop as BindingProp | BindableProp),
|
||||||
|
// ),
|
||||||
|
// )) {
|
||||||
|
// const updatedElement = nextElements.get(id);
|
||||||
|
// if (!updatedElement || updatedElement.isDeleted) {
|
||||||
|
// // skip fixing bindings for updates on deleted elements
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ElementsDelta.rebindAffected(prevElements, nextElements, id, updater);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // filter only previous elements, which were now affected
|
||||||
|
// const prevAffectedElements = new Map(
|
||||||
|
// Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // calculate complete deltas for affected elements, and assign them back to all the deltas
|
||||||
|
// // technically we could do better here if perf. would become an issue
|
||||||
|
// const { added, removed, updated } = ElementsDelta.calculate(
|
||||||
|
// prevAffectedElements,
|
||||||
|
// nextAffectedElements,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// for (const [id, delta] of Object.entries(added)) {
|
||||||
|
// this.added[id] = delta;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (const [id, delta] of Object.entries(removed)) {
|
||||||
|
// this.removed[id] = delta;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (const [id, delta] of Object.entries(updated)) {
|
||||||
|
// this.updated[id] = delta;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nextAffectedElements;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Non deleted affected elements of removed elements (before and after applying delta),
|
||||||
|
// * should be unbound ~ bindings should not point from non deleted into the deleted element/s.
|
||||||
|
// */
|
||||||
|
// private static unbindAffected(
|
||||||
|
// prevElements: SceneElementsMap,
|
||||||
|
// nextElements: SceneElementsMap,
|
||||||
|
// id: string,
|
||||||
|
// updater: (
|
||||||
|
// element: ExcalidrawElement,
|
||||||
|
// updates: ElementUpdate<ExcalidrawElement>,
|
||||||
|
// ) => void,
|
||||||
|
// ) {
|
||||||
|
// // the instance could have been updated, so make sure we are passing the latest element to each function below
|
||||||
|
// const prevElement = () => prevElements.get(id); // element before removal
|
||||||
|
// const nextElement = () => nextElements.get(id); // element after removal
|
||||||
|
|
||||||
|
// BoundElement.unbindAffected(nextElements, prevElement(), updater);
|
||||||
|
// BoundElement.unbindAffected(nextElements, nextElement(), updater);
|
||||||
|
|
||||||
|
// BindableElement.unbindAffected(nextElements, prevElement(), updater);
|
||||||
|
// BindableElement.unbindAffected(nextElements, nextElement(), updater);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Non deleted affected elements of added or updated element/s (before and after applying delta),
|
||||||
|
// * should be rebound (if possible) with the current element ~ bindings should be bidirectional.
|
||||||
|
// */
|
||||||
|
// private static rebindAffected(
|
||||||
|
// prevElements: SceneElementsMap,
|
||||||
|
// nextElements: SceneElementsMap,
|
||||||
|
// id: string,
|
||||||
|
// updater: (
|
||||||
|
// element: ExcalidrawElement,
|
||||||
|
// updates: ElementUpdate<ExcalidrawElement>,
|
||||||
|
// ) => void,
|
||||||
|
// ) {
|
||||||
|
// // the instance could have been updated, so make sure we are passing the latest element to each function below
|
||||||
|
// const prevElement = () => prevElements.get(id); // element before addition / update
|
||||||
|
// const nextElement = () => nextElements.get(id); // element after addition / update
|
||||||
|
|
||||||
|
// BoundElement.unbindAffected(nextElements, prevElement(), updater);
|
||||||
|
// BoundElement.rebindAffected(nextElements, nextElement(), updater);
|
||||||
|
|
||||||
|
// BindableElement.unbindAffected(
|
||||||
|
// nextElements,
|
||||||
|
// prevElement(),
|
||||||
|
// (element, updates) => {
|
||||||
|
// // we cannot rebind arrows with bindable element so we don't unbind them at all during rebind (we still need to unbind them on removal)
|
||||||
|
// // TODO: #7348 add startBinding / endBinding to the `BoundElement` context so that we could rebind arrows and remove this condition
|
||||||
|
// if (isTextElement(element)) {
|
||||||
|
// updater(element, updates);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private static redrawTextBoundingBoxes(
|
||||||
|
// elements: SceneElementsMap,
|
||||||
|
// changed: Map<string, OrderedExcalidrawElement>,
|
||||||
|
// ) {
|
||||||
|
// const boxesToRedraw = new Map<
|
||||||
|
// string,
|
||||||
|
// { container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
||||||
|
// >();
|
||||||
|
|
||||||
|
// for (const element of changed.values()) {
|
||||||
|
// if (isBoundToContainer(element)) {
|
||||||
|
// const { containerId } = element as ExcalidrawTextElement;
|
||||||
|
// const container = containerId ? elements.get(containerId) : undefined;
|
||||||
|
|
||||||
|
// if (container) {
|
||||||
|
// boxesToRedraw.set(container.id, {
|
||||||
|
// container,
|
||||||
|
// boundText: element as ExcalidrawTextElement,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (hasBoundTextElement(element)) {
|
||||||
|
// const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
// const boundText = boundTextElementId
|
||||||
|
// ? elements.get(boundTextElementId)
|
||||||
|
// : undefined;
|
||||||
|
|
||||||
|
// if (boundText) {
|
||||||
|
// boxesToRedraw.set(element.id, {
|
||||||
|
// container: element,
|
||||||
|
// boundText: boundText as ExcalidrawTextElement,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (const { container, boundText } of boxesToRedraw.values()) {
|
||||||
|
// if (container.isDeleted || boundText.isDeleted) {
|
||||||
|
// // skip redraw if one of them is deleted, as it would not result in a meaningful redraw
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// redrawTextBoundingBox(boundText, container, elements, false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private static redrawBoundArrows(
|
||||||
|
// elements: SceneElementsMap,
|
||||||
|
// changed: Map<string, OrderedExcalidrawElement>,
|
||||||
|
// ) {
|
||||||
|
// for (const element of changed.values()) {
|
||||||
|
// if (!element.isDeleted && isBindableElement(element)) {
|
||||||
|
// updateBoundElements(element, elements, {
|
||||||
|
// changedElements: changed,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private static reorderElements(
|
||||||
|
// elements: SceneElementsMap,
|
||||||
|
// changed: Map<string, OrderedExcalidrawElement>,
|
||||||
|
// flags: {
|
||||||
|
// containsVisibleDifference: boolean;
|
||||||
|
// containsZindexDifference: boolean;
|
||||||
|
// },
|
||||||
|
// ) {
|
||||||
|
// if (!flags.containsZindexDifference) {
|
||||||
|
// return elements;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const unordered = Array.from(elements.values());
|
||||||
|
// const ordered = orderByFractionalIndex([...unordered]);
|
||||||
|
// const moved = Delta.getRightDifferences(unordered, ordered, true).reduce(
|
||||||
|
// (acc, arrayIndex) => {
|
||||||
|
// const candidate = unordered[Number(arrayIndex)];
|
||||||
|
// if (candidate && changed.has(candidate.id)) {
|
||||||
|
// acc.set(candidate.id, candidate);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return acc;
|
||||||
|
// },
|
||||||
|
// new Map(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!flags.containsVisibleDifference && moved.size) {
|
||||||
|
// // we found a difference in order!
|
||||||
|
// flags.containsVisibleDifference = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // synchronize all elements that were actually moved
|
||||||
|
// // could fallback to synchronizing all invalid indices
|
||||||
|
// return elementsToMap(syncMovedIndices(ordered, moved)) as typeof elements;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is necessary to post process the partials in case of reference values,
|
||||||
|
* for which we need to calculate the real diff between `deleted` and `inserted`.
|
||||||
|
*/
|
||||||
|
private static postProcess(
|
||||||
|
deleted: ElementPartial,
|
||||||
|
inserted: ElementPartial,
|
||||||
|
): [ElementPartial, ElementPartial] {
|
||||||
|
try {
|
||||||
|
Delta.diffArrays(deleted, inserted, "boundElements", (x) => x.id);
|
||||||
|
} catch (e) {
|
||||||
|
// if postprocessing fails, it does not make sense to bubble up, but let's make sure we know about it
|
||||||
|
console.error(`Couldn't postprocess elements delta.`);
|
||||||
|
|
||||||
|
if (shouldThrow()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
return [deleted, inserted];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static stripIrrelevantProps(
|
||||||
|
partial: Partial<OrderedExcalidrawElement>,
|
||||||
|
): ElementPartial {
|
||||||
|
const { id, updated, version, versionNonce, ...strippedPartial } = partial;
|
||||||
|
|
||||||
|
return strippedPartial;
|
||||||
|
}
|
||||||
|
}
|
26
packages/deltas/src/excalidraw-types.d.ts
vendored
Normal file
26
packages/deltas/src/excalidraw-types.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export type {
|
||||||
|
AppState,
|
||||||
|
ObservedElementsAppState,
|
||||||
|
ObservedStandaloneAppState,
|
||||||
|
ObservedAppState,
|
||||||
|
} from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
|
export type {
|
||||||
|
DTO,
|
||||||
|
SubtypeOf,
|
||||||
|
ValueOf,
|
||||||
|
} from "@excalidraw/excalidraw/dist/excalidraw/utility-types";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
Ordered,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
SceneElementsMap,
|
||||||
|
ElementsMap,
|
||||||
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||||
|
export type { ElementUpdate } from "@excalidraw/excalidraw/dist/excalidraw/element/mutateElement";
|
||||||
|
export type {
|
||||||
|
BindableProp,
|
||||||
|
BindingProp,
|
||||||
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/binding";
|
5
packages/deltas/src/index.ts
Normal file
5
packages/deltas/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type { DeltaContainer } from "./common/interfaces";
|
||||||
|
|
||||||
|
export { Delta } from "./common/delta";
|
||||||
|
export { ElementsDelta } from "./containers/elements";
|
||||||
|
export { AppStateDelta } from "./containers/appstate";
|
19
packages/deltas/tsconfig.json
Normal file
19
packages/deltas/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist/types",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/*.test.*",
|
||||||
|
"**/tests/*",
|
||||||
|
"types",
|
||||||
|
"dist",
|
||||||
|
],
|
||||||
|
}
|
3
packages/element/.eslintrc.json
Normal file
3
packages/element/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../eslintrc.base.json"]
|
||||||
|
}
|
19
packages/element/README.md
Normal file
19
packages/element/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @excalidraw/element
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/element
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/element
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/element
|
||||||
|
```
|
3
packages/element/global.d.ts
vendored
Normal file
3
packages/element/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
import "@excalidraw/excalidraw/global";
|
||||||
|
import "@excalidraw/excalidraw/css";
|
56
packages/element/package.json
Normal file
56
packages/element/package.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "@excalidraw/element",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/types/element/src/index.d.ts",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/types/element/src/index.d.ts",
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"production": "./dist/prod/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./../element/dist/types/element/src/*.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw elements-related logic",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-utils"
|
||||||
|
],
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all",
|
||||||
|
"not safari < 12",
|
||||||
|
"not kaios <= 2.5",
|
||||||
|
"not edge < 79",
|
||||||
|
"not chrome < 70",
|
||||||
|
"not and_uc < 13",
|
||||||
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,31 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import {
|
||||||
|
randomInteger,
|
||||||
|
arrayToMap,
|
||||||
|
toBrandedType,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
|
isReadonlyArray,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
|
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||||
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import {
|
||||||
|
orderByFractionalIndex,
|
||||||
|
syncInvalidIndices,
|
||||||
|
syncMovedIndices,
|
||||||
|
validateFractionalIndices,
|
||||||
|
} from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
|
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||||
|
|
||||||
|
import {
|
||||||
|
mutateElement,
|
||||||
|
type ElementUpdate,
|
||||||
|
} from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@@ -9,26 +36,15 @@ import type {
|
|||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
Ordered,
|
Ordered,
|
||||||
} from "../element/types";
|
} from "@excalidraw/element/types";
|
||||||
import { isNonDeletedElement } from "../element";
|
|
||||||
import type { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
|
||||||
import { getSelectedElements } from "./selection";
|
|
||||||
import type { AppState } from "../types";
|
|
||||||
import type { Assert, SameType } from "../utility-types";
|
|
||||||
import { randomInteger } from "../random";
|
|
||||||
import {
|
|
||||||
syncInvalidIndices,
|
|
||||||
syncMovedIndices,
|
|
||||||
validateFractionalIndices,
|
|
||||||
} from "../fractionalIndex";
|
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
import { toBrandedType } from "../utils";
|
|
||||||
import { ENV } from "../constants";
|
|
||||||
import { getElementsInGroup } from "../groups";
|
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
import type {
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
Assert,
|
||||||
|
Mutable,
|
||||||
|
SameType,
|
||||||
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { AppState } from "../../excalidraw/types";
|
||||||
|
|
||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
@@ -54,14 +70,10 @@ const getNonDeletedElements = <T extends ExcalidrawElement>(
|
|||||||
|
|
||||||
const validateIndicesThrottled = throttle(
|
const validateIndicesThrottled = throttle(
|
||||||
(elements: readonly ExcalidrawElement[]) => {
|
(elements: readonly ExcalidrawElement[]) => {
|
||||||
if (
|
if (isDevEnv() || isTestEnv() || window?.DEBUG_FRACTIONAL_INDICES) {
|
||||||
import.meta.env.DEV ||
|
|
||||||
import.meta.env.MODE === ENV.TEST ||
|
|
||||||
window?.DEBUG_FRACTIONAL_INDICES
|
|
||||||
) {
|
|
||||||
validateFractionalIndices(elements, {
|
validateFractionalIndices(elements, {
|
||||||
// throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
|
// throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
|
||||||
shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
|
shouldThrow: isDevEnv() || isTestEnv(),
|
||||||
includeBoundTextValidation: true,
|
includeBoundTextValidation: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,44 +109,7 @@ const hashSelectionOpts = (
|
|||||||
// in our codebase
|
// in our codebase
|
||||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||||
|
|
||||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
|
||||||
if (typeof elementKey === "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Scene {
|
class Scene {
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// static methods/props
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
|
||||||
private static sceneMapById = new Map<string, Scene>();
|
|
||||||
|
|
||||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
// for cases where we don't have access to the element object
|
|
||||||
// (e.g. restore serialized appState with id references)
|
|
||||||
this.sceneMapById.set(elementKey, scene);
|
|
||||||
} else {
|
|
||||||
this.sceneMapByElement.set(elementKey, scene);
|
|
||||||
// if mapping element objects, also cache the id string when later
|
|
||||||
// looking up by id alone
|
|
||||||
this.sceneMapById.set(elementKey.id, scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated pass down `app.scene` and use it directly
|
|
||||||
*/
|
|
||||||
static getScene(elementKey: ElementKey): Scene | null {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
return this.sceneMapById.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
return this.sceneMapByElement.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// instance methods/props
|
// instance methods/props
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -193,6 +168,12 @@ class Scene {
|
|||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(elements: ElementsMapOrArray | null = null) {
|
||||||
|
if (elements) {
|
||||||
|
this.replaceAllElements(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSelectedElements(opts: {
|
getSelectedElements(opts: {
|
||||||
// NOTE can be ommitted by making Scene constructor require App instance
|
// NOTE can be ommitted by making Scene constructor require App instance
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
@@ -287,23 +268,25 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
const _nextElements =
|
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
if (!isReadonlyArray(nextElements)) {
|
||||||
nextElements instanceof Array
|
// need to order by fractional indices to get the correct order
|
||||||
? nextElements
|
nextElements = orderByFractionalIndex(
|
||||||
: Array.from(nextElements.values());
|
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(_nextElements);
|
validateIndicesThrottled(nextElements);
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
this.elements.forEach((element) => {
|
this.elements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
nextFrameLikes.push(element);
|
nextFrameLikes.push(element);
|
||||||
}
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
|
||||||
});
|
});
|
||||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
this.nonDeletedElements = nonDeletedElements.elements;
|
this.nonDeletedElements = nonDeletedElements.elements;
|
||||||
@@ -348,12 +331,6 @@ class Scene {
|
|||||||
this.selectedElementsCache.elements = null;
|
this.selectedElementsCache.elements = null;
|
||||||
this.selectedElementsCache.cache.clear();
|
this.selectedElementsCache.cache.clear();
|
||||||
|
|
||||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
|
||||||
if (scene === this) {
|
|
||||||
Scene.sceneMapById.delete(elementKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
@@ -450,6 +427,42 @@ class Scene {
|
|||||||
// then, check if the id is a group
|
// then, check if the id is a group
|
||||||
return getElementsInGroup(elementsMap, id);
|
return getElementsInGroup(elementsMap, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||||
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
|
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
options: {
|
||||||
|
informMutation: boolean;
|
||||||
|
isDragging: boolean;
|
||||||
|
} = {
|
||||||
|
informMutation: true,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const elementsMap = this.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
const { version: prevVersion } = element;
|
||||||
|
const { version: nextVersion } = mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
updates,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// skip if the element is not in the scene (i.e. selection)
|
||||||
|
this.elementsMap.has(element.id) &&
|
||||||
|
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||||
|
prevVersion !== nextVersion &&
|
||||||
|
options.informMutation
|
||||||
|
) {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
@@ -1,31 +1,38 @@
|
|||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
|
||||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
|
||||||
import type { ElementShapes } from "./types";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
ExcalidrawSelectionElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
Arrowhead,
|
|
||||||
} from "../element/types";
|
|
||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
|
||||||
import { isTransparent, assertNever } from "../utils";
|
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
|
||||||
|
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||||
|
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { ElementShapes } from "@excalidraw/excalidraw/scene/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isIframeElement,
|
isIframeElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "./typeChecks";
|
||||||
|
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||||
|
import { headingForPointIsHorizontal } from "./heading";
|
||||||
|
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import { generateFreeDrawShape } from "./renderElement";
|
||||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
|
||||||
import { headingForPointIsHorizontal } from "../element/heading";
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
ExcalidrawSelectionElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
Arrowhead,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@@ -508,7 +515,10 @@ export const _generateElementShape = (
|
|||||||
|
|
||||||
if (isPathALoop(element.points)) {
|
if (isPathALoop(element.points)) {
|
||||||
// generate rough polygon to fill freedraw shape
|
// generate rough polygon to fill freedraw shape
|
||||||
const simplifiedPoints = simplify(element.points, 0.75);
|
const simplifiedPoints = simplify(
|
||||||
|
element.points as Mutable<LocalPoint[]>,
|
||||||
|
0.75,
|
||||||
|
);
|
||||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||||
...generateRoughOptions(element),
|
...generateRoughOptions(element),
|
||||||
stroke: "none",
|
stroke: "none",
|
@@ -1,14 +1,23 @@
|
|||||||
import type { Drawable } from "roughjs/bin/core";
|
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
|
import { COLOR_PALETTE } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
AppState,
|
||||||
ExcalidrawSelectionElement,
|
EmbedsValidationStatus,
|
||||||
} from "../element/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import { elementWithCanvasCache } from "../renderer/renderElement";
|
import type {
|
||||||
|
ElementShape,
|
||||||
|
ElementShapes,
|
||||||
|
} from "@excalidraw/excalidraw/scene/types";
|
||||||
|
|
||||||
import { _generateElementShape } from "./Shape";
|
import { _generateElementShape } from "./Shape";
|
||||||
import type { ElementShape, ElementShapes } from "./types";
|
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { elementWithCanvasCache } from "./renderElement";
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
|
||||||
|
import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types";
|
||||||
|
|
||||||
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
||||||
private static rg = new RoughGenerator();
|
private static rg = new RoughGenerator();
|
@@ -1,10 +1,11 @@
|
|||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import { updateBoundElements } from "./binding";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
import type { BoundingBox } from "./element/bounds";
|
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
import { updateBoundElements } from "./element/binding";
|
|
||||||
import type Scene from "./scene/Scene";
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
|
import type { BoundingBox } from "./bounds";
|
||||||
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
@@ -13,10 +14,10 @@ export interface Alignment {
|
|||||||
|
|
||||||
export const alignElements = (
|
export const alignElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -31,12 +32,13 @@ export const alignElements = (
|
|||||||
);
|
);
|
||||||
return group.map((element) => {
|
return group.map((element) => {
|
||||||
// update element
|
// update element
|
||||||
const updatedEle = mutateElement(element, {
|
const updatedEle = scene.mutateElement(element, {
|
||||||
x: element.x + translation.x,
|
x: element.x + translation.x,
|
||||||
y: element.y + translation.y,
|
y: element.y + translation.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bound elements
|
// update bound elements
|
||||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: group,
|
simultaneouslyUpdated: group,
|
||||||
});
|
});
|
||||||
return updatedEle;
|
return updatedEle;
|
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,12 @@
|
|||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
Arrowhead,
|
|
||||||
ExcalidrawFreeDrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
ElementsMap,
|
|
||||||
} from "./types";
|
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
|
||||||
import type { AppState } from "../types";
|
|
||||||
import { generateRoughOptions } from "../scene/Shape";
|
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
rescalePoints,
|
||||||
isBoundToContainer,
|
arrayToMap,
|
||||||
isFreeDrawElement,
|
invariant,
|
||||||
isLinearElement,
|
sizeOf,
|
||||||
isTextElement,
|
} from "@excalidraw/common";
|
||||||
} from "./typeChecks";
|
|
||||||
import { rescalePoints } from "../points";
|
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
import { arrayToMap, invariant } from "../utils";
|
|
||||||
import type {
|
|
||||||
Degrees,
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
@@ -39,8 +15,57 @@ import {
|
|||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Curve,
|
||||||
|
Degrees,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
|
Radians,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
import { generateRoughOptions } from "./Shape";
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import { getElementShape } from "./shapes";
|
||||||
|
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
|
} from "./types";
|
||||||
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
export type RectangleBox = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -247,50 +272,82 @@ export const getElementAbsoluteCoords = (
|
|||||||
* that can be used for visual collision detection (useful for frames)
|
* that can be used for visual collision detection (useful for frames)
|
||||||
* as opposed to bounding box collision detection
|
* as opposed to bounding box collision detection
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Given an element, return the line segments that make up the element.
|
||||||
|
*
|
||||||
|
* Uses helpers from /math
|
||||||
|
*/
|
||||||
export const getElementLineSegments = (
|
export const getElementLineSegments = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): LineSegment<GlobalPoint>[] => {
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const shape = getElementShape(element, elementsMap);
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
const center: GlobalPoint = pointFrom(cx, cy);
|
if (shape.type === "polycurve") {
|
||||||
|
const curves = shape.data;
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
const points = curves
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||||
|
.flat();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
while (i < element.points.length - 1) {
|
while (i < points.length - 1) {
|
||||||
segments.push(
|
segments.push(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(
|
pointFrom(points[i][0], points[i][1]),
|
||||||
pointFrom(
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
element.points[i][0] + element.x,
|
|
||||||
element.points[i][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
pointRotateRads(
|
|
||||||
pointFrom(
|
|
||||||
element.points[i + 1][0] + element.x,
|
|
||||||
element.points[i + 1][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
|
} else if (shape.type === "polyline") {
|
||||||
|
return shape.data as LineSegment<GlobalPoint>[];
|
||||||
|
} else if (_isRectanguloidElement(element)) {
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||||
|
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
|
const [sides, corners] = deconstructDiamondElement(element);
|
||||||
|
const cornerSegments = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (shape.type === "polygon") {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
if (container && isLinearElement(container)) {
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [
|
||||||
|
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||||
|
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||||
|
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||||
|
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||||
|
];
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = shape.data as GlobalPoint[];
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
} else if (shape.type === "ellipse") {
|
||||||
|
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nw, ne, sw, se, n, s, w, e] = (
|
const [nw, ne, sw, se, , , w, e] = (
|
||||||
[
|
[
|
||||||
[x1, y1],
|
[x1, y1],
|
||||||
[x2, y1],
|
[x2, y1],
|
||||||
@@ -303,28 +360,6 @@ export const getElementLineSegments = (
|
|||||||
] as GlobalPoint[]
|
] as GlobalPoint[]
|
||||||
).map((point) => pointRotateRads(point, center, element.angle));
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
lineSegment(nw, ne),
|
lineSegment(nw, ne),
|
||||||
lineSegment(sw, se),
|
lineSegment(sw, se),
|
||||||
@@ -337,6 +372,94 @@ export const getElementLineSegments = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _isRectanguloidElement = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawRectanguloidElement => {
|
||||||
|
return (
|
||||||
|
element != null &&
|
||||||
|
(element.type === "rectangle" ||
|
||||||
|
element.type === "image" ||
|
||||||
|
element.type === "iframe" ||
|
||||||
|
element.type === "embeddable" ||
|
||||||
|
element.type === "frame" ||
|
||||||
|
element.type === "magicframe" ||
|
||||||
|
(element.type === "text" && !element.containerId))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotatedSides = (
|
||||||
|
sides: LineSegment<GlobalPoint>[],
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
) => {
|
||||||
|
return sides.map((side) => {
|
||||||
|
return lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||||
|
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnCurve = (
|
||||||
|
curve: Curve<GlobalPoint>,
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const points = pointsOnBezierCurves(curve, 10);
|
||||||
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
while (i < points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i][0], points[i][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnEllipse = (
|
||||||
|
ellipse: ExcalidrawEllipseElement,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
ellipse.x + ellipse.width / 2,
|
||||||
|
ellipse.y + ellipse.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const a = ellipse.width / 2;
|
||||||
|
const b = ellipse.height / 2;
|
||||||
|
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
const points: GlobalPoint[] = [];
|
||||||
|
const n = 90;
|
||||||
|
const deltaT = (Math.PI * 2) / n;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const t = i * deltaT;
|
||||||
|
const x = center[0] + a * Math.cos(t);
|
||||||
|
const y = center[1] + b * Math.sin(t);
|
||||||
|
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||||
*
|
*
|
||||||
@@ -821,10 +944,10 @@ export const getElementBounds = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
elementsMap?: ElementsMap,
|
elementsMap?: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!sizeOf(elements)) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
@@ -1,31 +1,4 @@
|
|||||||
import type {
|
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawDiamondElement,
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawEllipseElement,
|
|
||||||
ExcalidrawRectangleElement,
|
|
||||||
ExcalidrawRectanguloidElement,
|
|
||||||
} from "./types";
|
|
||||||
import { getElementBounds } from "./bounds";
|
|
||||||
import type { FrameNameBounds } from "../types";
|
|
||||||
import type { GeometricShape } from "@excalidraw/utils/geometry/shape";
|
|
||||||
import { getPolygonShape } from "@excalidraw/utils/geometry/shape";
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
|
||||||
import { isTransparent } from "../utils";
|
|
||||||
import {
|
|
||||||
hasBoundTextElement,
|
|
||||||
isIframeLikeElement,
|
|
||||||
isImageElement,
|
|
||||||
isTextElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
|
||||||
import type {
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Polygon,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
@@ -36,15 +9,47 @@ import {
|
|||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseLineIntersectionPoints,
|
ellipseLineIntersectionPoints,
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
|
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
|
Polygon,
|
||||||
|
Radians,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||||
|
import { getElementBounds } from "./bounds";
|
||||||
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isIframeLikeElement,
|
||||||
|
isImageElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawRectangleElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return false;
|
return false;
|
||||||
@@ -184,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
@@ -205,28 +207,28 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
// Test intersection against the sides, keep only the valid
|
||||||
// Test intersection against the sides, keep only the valid
|
// intersection points and rotate them back to scene space
|
||||||
// intersection points and rotate them back to scene space
|
sides
|
||||||
...sides
|
.map((s) =>
|
||||||
.map((s) =>
|
lineSegmentIntersectionPoints(
|
||||||
lineSegmentIntersectionPoints(
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
s,
|
||||||
s,
|
),
|
||||||
),
|
)
|
||||||
)
|
.filter((x) => x != null)
|
||||||
.filter((x) => x != null)
|
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
|
||||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
|
|
||||||
// Test intersection against the corners which are cubic bezier curves,
|
// Test intersection against the corners which are cubic bezier curves,
|
||||||
// keep only the valid intersection points and rotate them back to scene
|
// keep only the valid intersection points and rotate them back to scene
|
||||||
// space
|
// space
|
||||||
...corners
|
.concat(
|
||||||
.flatMap((t) =>
|
corners
|
||||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
.flatMap((t) =>
|
||||||
)
|
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
||||||
.filter((i) => i != null)
|
)
|
||||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
.filter((i) => i != null)
|
||||||
]
|
.map((j) => pointRotateRads(j, center, element.angle)),
|
||||||
|
)
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
@@ -246,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
|||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
@@ -259,25 +258,25 @@ const intersectDiamondWithLineSegment = (
|
|||||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
const [sides, curves] = deconstructDiamondElement(element, offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
sides
|
||||||
...sides
|
.map((s) =>
|
||||||
.map((s) =>
|
lineSegmentIntersectionPoints(
|
||||||
lineSegmentIntersectionPoints(
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
s,
|
||||||
s,
|
),
|
||||||
),
|
)
|
||||||
)
|
.filter((p): p is GlobalPoint => p != null)
|
||||||
.filter((p): p is GlobalPoint => p != null)
|
// Rotate back intersection points
|
||||||
// Rotate back intersection points
|
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
|
||||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
|
.concat(
|
||||||
...curves
|
curves
|
||||||
.flatMap((p) =>
|
.flatMap((p) =>
|
||||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
||||||
)
|
)
|
||||||
.filter((p) => p != null)
|
.filter((p) => p != null)
|
||||||
// Rotate back intersection points
|
// Rotate back intersection points
|
||||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
.map((p) => pointRotateRads(p, center, element.angle)),
|
||||||
]
|
)
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
@@ -297,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
|||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
@@ -1,4 +1,4 @@
|
|||||||
import type { ElementOrToolType } from "../types";
|
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
export const hasBackground = (type: ElementOrToolType) =>
|
export const hasBackground = (type: ElementOrToolType) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user