Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c3dffb082 | ||
|
|
0e0f34edd8 | ||
|
|
4888d9d355 | ||
|
|
1c39bd5781 | ||
|
|
90ad885446 | ||
|
|
1741c234a6 | ||
|
|
63b50b3586 | ||
|
|
e0fefa8025 | ||
|
|
d426cc968d | ||
|
|
2409c091ff | ||
|
|
626fe252ab | ||
|
|
10bd08ef19 | ||
|
|
2789d08154 | ||
|
|
678bb2b819 | ||
|
|
966f9aead9 | ||
|
|
4f0a2a9593 | ||
|
|
f3f8217125 | ||
|
|
89bd6181f2 | ||
|
|
c6fdac131b | ||
|
|
0415c616b1 | ||
|
|
740a165452 | ||
|
|
4997624a3a | ||
|
|
b66daae1f3 | ||
|
|
1e7df58b5b | ||
|
|
46da032626 | ||
|
|
3b0593baa7 | ||
|
|
dd530737a2 | ||
|
|
a4e5e46dd1 | ||
|
|
0fa5f5de4c | ||
|
|
41cc746885 | ||
|
|
8ead8559e0 | ||
|
|
5245276409 | ||
|
|
0c24a7042f | ||
|
|
86cfeb714c | ||
|
|
872973f145 | ||
|
|
3ecf72a507 | ||
|
|
1aaa400876 | ||
|
|
65047cc2cb | ||
|
|
8b993d409e | ||
|
|
1cb350b2aa | ||
|
|
43ccc875fb | ||
|
|
4249b7dec8 |
@@ -7,9 +7,6 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
|||||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||||
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
|
|
||||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
|
||||||
VITE_APP_PORTAL_URL=
|
|
||||||
|
|
||||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
|||||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
|
||||||
|
|
||||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
|
||||||
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com
|
||||||
|
|
||||||
# Fill to set socket server URL used for collaboration.
|
# socket server URL used for collaboration
|
||||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
||||||
VITE_APP_WS_SERVER_URL=
|
|
||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ jobs:
|
|||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
- name: Auto release
|
- name: Auto release
|
||||||
run: |
|
run: |
|
||||||
yarn add @actions/core
|
yarn add @actions/core -W
|
||||||
yarn autorelease
|
yarn autorelease
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ packages/excalidraw/types
|
|||||||
coverage
|
coverage
|
||||||
dev-dist
|
dev-dist
|
||||||
html
|
html
|
||||||
|
examples/**/bundle.*
|
||||||
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
|||||||
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
|
||||||
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
|
||||||
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
|
||||||
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
|
| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
|
||||||
| [onChange](#onChange) | `function` | Subscribes to change events |
|
| [onChange](#onChange) | `function` | Subscribes to change events |
|
||||||
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
|
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
|
||||||
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
|
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ All `props` are _optional_.
|
|||||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||||
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
||||||
| [`name`](#name) | `string` | | Name of the drawing |
|
| [`name`](#name) | `string` | | Name of the drawing |
|
||||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
|
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
|
||||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
||||||
| [`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 |
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ function App() {
|
|||||||
|
|
||||||
## tools
|
## tools
|
||||||
|
|
||||||
This `prop ` controls the visibility of the tools in the editor.
|
This `prop` controls the visibility of the tools in the editor.
|
||||||
Currently you can control the visibility of `image` tool via this prop.
|
Currently you can control the visibility of `image` tool via this prop.
|
||||||
|
|
||||||
| Prop | Type | Default | Description |
|
| Prop | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| image | boolean | true | Decides whether `image` tool should be visible.
|
| image | boolean | true | Decides whether `image` tool should be visible.
|
||||||
|
|||||||
@@ -32,15 +32,9 @@ function App() {
|
|||||||
|
|
||||||
### Next.js
|
### Next.js
|
||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`.
|
||||||
|
|
||||||
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
If you want to only import `Excalidraw` component you can do :point_down:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1. Using **Next.js Dynamic** import [Recommended].
|
|
||||||
|
|
||||||
Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
|
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -55,25 +49,88 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically.
|
||||||
|
|
||||||
|
If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down:
|
||||||
|
|
||||||
2. Importing Excalidraw once **client** is rendered.
|
<Tabs>
|
||||||
|
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
export default function App() {
|
import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||||
const [Excalidraw, setExcalidraw] = useState(null);
|
|
||||||
useEffect(() => {
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import("@excalidraw/excalidraw").then((comp) =>
|
|
||||||
setExcalidraw(comp.Excalidraw),
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
|
console.info(convertToExcalidrawElements([{
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rect-1",
|
||||||
|
width: 186.47265625,
|
||||||
|
height: 141.9765625,
|
||||||
|
},]));
|
||||||
|
return (
|
||||||
|
<div style={{height:"500px", width:"500px"}}
|
||||||
|
<Excalidraw />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
return <>{Excalidraw && <Excalidraw />}</>;
|
export default ExcalidrawWrapper;
|
||||||
}
|
```
|
||||||
```
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="pages" label="Pages router">
|
||||||
|
|
||||||
|
```jsx showLineNumbers
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing
|
||||||
|
// the excalidraw stuff dynamically with ssr false
|
||||||
|
|
||||||
|
const ExcalidrawWrapper = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<ExcalidrawWrapper />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="app" label="App router">
|
||||||
|
|
||||||
|
```jsx showLineNumbers
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing
|
||||||
|
// the excalidraw stuff dynamically with ssr false
|
||||||
|
|
||||||
|
const ExcalidrawWrapper = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<ExcalidrawWrapper />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/).
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
|
||||||
|
|
||||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,23 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.app-title {
|
||||||
|
margin-block-start: 0.83em;
|
||||||
|
margin-block-end: 0.83em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper button {
|
.button-wrapper {
|
||||||
z-index: 1;
|
input[type="checkbox"] {
|
||||||
height: 40px;
|
margin: 5px;
|
||||||
max-width: 200px;
|
}
|
||||||
margin: 10px;
|
button {
|
||||||
padding: 5px;
|
z-index: 1;
|
||||||
|
height: 40px;
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .App-menu_top .buttonList {
|
.excalidraw .App-menu_top .buttonList {
|
||||||
@@ -1,20 +1,30 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
Children,
|
||||||
|
cloneElement,
|
||||||
|
} from "react";
|
||||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||||
|
|
||||||
import type * as TExcalidraw from "../index";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import "./App.scss";
|
|
||||||
import initialData from "./initialData";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
ResolvablePromise,
|
ResolvablePromise,
|
||||||
|
distance2d,
|
||||||
|
fileOpen,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
withBatchedUpdatesThrottled,
|
withBatchedUpdatesThrottled,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EVENT, ROUNDNESS } from "../constants";
|
|
||||||
import { distance2d } from "../math";
|
import CustomFooter from "./CustomFooter";
|
||||||
import { fileOpen } from "../data/filesystem";
|
import MobileFooter from "./MobileFooter";
|
||||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
import initialData from "../initialData";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
@@ -23,18 +33,14 @@ import type {
|
|||||||
Gesture,
|
Gesture,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
PointerDownState as ExcalidrawPointerDownState,
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
} from "../types";
|
} from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
import type { NonDeletedExcalidrawElement, Theme } from "../element/types";
|
import type {
|
||||||
import { ImportedLibraryData } from "../data/types";
|
NonDeletedExcalidrawElement,
|
||||||
import CustomFooter from "./CustomFooter";
|
Theme,
|
||||||
import MobileFooter from "./MobileFooter";
|
} from "@excalidraw/excalidraw/dist/excalidraw/element/types";
|
||||||
import { KEYS } from "../keys";
|
import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types";
|
||||||
|
|
||||||
declare global {
|
import "./App.scss";
|
||||||
interface Window {
|
|
||||||
ExcalidrawLib: typeof TExcalidraw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment = {
|
type Comment = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -55,31 +61,6 @@ type PointerDownState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const { useEffect, useState, useRef, useCallback } = window.React;
|
|
||||||
|
|
||||||
// This is so that we use the bundled excalidraw.development.js file instead
|
|
||||||
// of the actual source code
|
|
||||||
const {
|
|
||||||
exportToCanvas,
|
|
||||||
exportToSvg,
|
|
||||||
exportToBlob,
|
|
||||||
exportToClipboard,
|
|
||||||
Excalidraw,
|
|
||||||
useHandleLibrary,
|
|
||||||
MIME_TYPES,
|
|
||||||
sceneCoordsToViewportCoords,
|
|
||||||
viewportCoordsToSceneCoords,
|
|
||||||
restoreElements,
|
|
||||||
Sidebar,
|
|
||||||
Footer,
|
|
||||||
WelcomeScreen,
|
|
||||||
MainMenu,
|
|
||||||
LiveCollaborationTrigger,
|
|
||||||
convertToExcalidrawElements,
|
|
||||||
TTDDialog,
|
|
||||||
TTDDialogTrigger,
|
|
||||||
} = window.ExcalidrawLib;
|
|
||||||
|
|
||||||
const COMMENT_ICON_DIMENSION = 32;
|
const COMMENT_ICON_DIMENSION = 32;
|
||||||
const COMMENT_INPUT_HEIGHT = 50;
|
const COMMENT_INPUT_HEIGHT = 50;
|
||||||
const COMMENT_INPUT_WIDTH = 150;
|
const COMMENT_INPUT_WIDTH = 150;
|
||||||
@@ -88,8 +69,38 @@ export interface AppProps {
|
|||||||
appTitle: string;
|
appTitle: string;
|
||||||
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
||||||
customArgs?: any[];
|
customArgs?: any[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}
|
}
|
||||||
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|
||||||
|
export default function App({
|
||||||
|
appTitle,
|
||||||
|
useCustom,
|
||||||
|
customArgs,
|
||||||
|
children,
|
||||||
|
excalidrawLib,
|
||||||
|
}: AppProps) {
|
||||||
|
const {
|
||||||
|
exportToCanvas,
|
||||||
|
exportToSvg,
|
||||||
|
exportToBlob,
|
||||||
|
exportToClipboard,
|
||||||
|
useHandleLibrary,
|
||||||
|
MIME_TYPES,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
restoreElements,
|
||||||
|
Sidebar,
|
||||||
|
Footer,
|
||||||
|
WelcomeScreen,
|
||||||
|
MainMenu,
|
||||||
|
LiveCollaborationTrigger,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
TTDDialog,
|
||||||
|
TTDDialogTrigger,
|
||||||
|
ROUNDNESS,
|
||||||
|
loadSceneOrLibraryFromBlob,
|
||||||
|
} = excalidrawLib;
|
||||||
const appRef = useRef<any>(null);
|
const appRef = useRef<any>(null);
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
@@ -151,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
|
||||||
|
|
||||||
|
const renderExcalidraw = (children: React.ReactNode) => {
|
||||||
|
const Excalidraw: any = Children.toArray(children).find(
|
||||||
|
(child) =>
|
||||||
|
React.isValidElement(child) &&
|
||||||
|
typeof child.type !== "string" &&
|
||||||
|
//@ts-ignore
|
||||||
|
child.type.displayName === "Excalidraw",
|
||||||
|
);
|
||||||
|
if (!Excalidraw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newElement = cloneElement(
|
||||||
|
Excalidraw,
|
||||||
|
{
|
||||||
|
excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
|
||||||
|
initialData: initialStatePromiseRef.current.promise,
|
||||||
|
onChange: (
|
||||||
|
elements: NonDeletedExcalidrawElement[],
|
||||||
|
state: AppState,
|
||||||
|
) => {
|
||||||
|
console.info("Elements :", elements, "State : ", state);
|
||||||
|
},
|
||||||
|
onPointerUpdate: (payload: {
|
||||||
|
pointer: { x: number; y: number };
|
||||||
|
button: "down" | "up";
|
||||||
|
pointersMap: Gesture["pointers"];
|
||||||
|
}) => setPointerData(payload),
|
||||||
|
viewModeEnabled,
|
||||||
|
zenModeEnabled,
|
||||||
|
gridModeEnabled,
|
||||||
|
theme,
|
||||||
|
name: "Custom name of drawing",
|
||||||
|
UIOptions: {
|
||||||
|
canvasActions: {
|
||||||
|
loadScene: false,
|
||||||
|
},
|
||||||
|
tools: { image: !disableImageTool },
|
||||||
|
},
|
||||||
|
renderTopRightUI,
|
||||||
|
onLinkOpen,
|
||||||
|
onPointerDown,
|
||||||
|
onScrollChange: rerenderCommentIcons,
|
||||||
|
validateEmbeddable: true,
|
||||||
|
},
|
||||||
|
<>
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<Footer>
|
||||||
|
<CustomFooter
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
/>
|
||||||
|
</Footer>
|
||||||
|
)}
|
||||||
|
<WelcomeScreen />
|
||||||
|
<Sidebar name="custom">
|
||||||
|
<Sidebar.Tabs>
|
||||||
|
<Sidebar.Header />
|
||||||
|
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
||||||
|
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
||||||
|
<Sidebar.TabTriggers>
|
||||||
|
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
||||||
|
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
||||||
|
</Sidebar.TabTriggers>
|
||||||
|
</Sidebar.Tabs>
|
||||||
|
</Sidebar>
|
||||||
|
<Sidebar.Trigger
|
||||||
|
name="custom"
|
||||||
|
tab="one"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
bottom: "20px",
|
||||||
|
zIndex: 9999999999999999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Toggle Custom Sidebar
|
||||||
|
</Sidebar.Trigger>
|
||||||
|
{renderMenu()}
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<TTDDialogTrigger icon={<span>😀</span>}>
|
||||||
|
Text to diagram
|
||||||
|
</TTDDialogTrigger>
|
||||||
|
)}
|
||||||
|
<TTDDialog
|
||||||
|
onTextSubmit={async (_) => {
|
||||||
|
console.info("submit");
|
||||||
|
// sleep for 2s
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
throw new Error("error, go away now");
|
||||||
|
// return "dummy";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
return newElement;
|
||||||
|
};
|
||||||
const renderTopRightUI = (isMobile: boolean) => {
|
const renderTopRightUI = (isMobile: boolean) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -336,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) => {
|
) => {
|
||||||
return withBatchedUpdates((event) => {
|
return withBatchedUpdates((event) => {
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
window.removeEventListener("pointermove", pointerDownState.onMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
window.removeEventListener("pointerup", pointerDownState.onUp);
|
||||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||||
const distance = distance2d(
|
const distance = distance2d(
|
||||||
pointerDownState.x,
|
pointerDownState.x,
|
||||||
@@ -401,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
onPointerMoveFromPointerDownHandler(pointerDownState);
|
onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||||
const onPointerUp =
|
const onPointerUp =
|
||||||
onPointerUpFromPointerDownHandler(pointerDownState);
|
onPointerUpFromPointerDownHandler(pointerDownState);
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.addEventListener("pointermove", onPointerMove);
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
|
|
||||||
pointerDownState.onMove = onPointerMove;
|
pointerDownState.onMove = onPointerMove;
|
||||||
pointerDownState.onUp = onPointerUp;
|
pointerDownState.onUp = onPointerUp;
|
||||||
@@ -494,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
}}
|
}}
|
||||||
onBlur={saveComment}
|
onBlur={saveComment}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (!event.shiftKey && event.key === KEYS.ENTER) {
|
if (!event.shiftKey && event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
saveComment();
|
saveComment();
|
||||||
}
|
}
|
||||||
@@ -527,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
|
|
||||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && (
|
||||||
|
<MobileFooter
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -676,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper">
|
||||||
<Excalidraw
|
{renderExcalidraw(children)}
|
||||||
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
|
||||||
setExcalidrawAPI(api)
|
|
||||||
}
|
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
|
||||||
onChange={(elements, state) => {
|
|
||||||
// console.info("Elements :", elements, "State : ", state);
|
|
||||||
}}
|
|
||||||
onPointerUpdate={(payload: {
|
|
||||||
pointer: { x: number; y: number };
|
|
||||||
button: "down" | "up";
|
|
||||||
pointersMap: Gesture["pointers"];
|
|
||||||
}) => setPointerData(payload)}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
|
||||||
zenModeEnabled={zenModeEnabled}
|
|
||||||
gridModeEnabled={gridModeEnabled}
|
|
||||||
theme={theme}
|
|
||||||
name="Custom name of drawing"
|
|
||||||
UIOptions={{
|
|
||||||
canvasActions: {
|
|
||||||
loadScene: false,
|
|
||||||
},
|
|
||||||
tools: { image: !disableImageTool },
|
|
||||||
}}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
|
||||||
onLinkOpen={onLinkOpen}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
onScrollChange={rerenderCommentIcons}
|
|
||||||
// allow all urls
|
|
||||||
validateEmbeddable={true}
|
|
||||||
>
|
|
||||||
{excalidrawAPI && (
|
|
||||||
<Footer>
|
|
||||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
|
||||||
</Footer>
|
|
||||||
)}
|
|
||||||
<WelcomeScreen />
|
|
||||||
<Sidebar name="custom">
|
|
||||||
<Sidebar.Tabs>
|
|
||||||
<Sidebar.Header />
|
|
||||||
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
|
||||||
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
|
||||||
<Sidebar.TabTriggers>
|
|
||||||
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
|
||||||
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
|
||||||
</Sidebar.TabTriggers>
|
|
||||||
</Sidebar.Tabs>
|
|
||||||
</Sidebar>
|
|
||||||
<Sidebar.Trigger
|
|
||||||
name="custom"
|
|
||||||
tab="one"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
bottom: "20px",
|
|
||||||
zIndex: 9999999999999999,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Custom Sidebar
|
|
||||||
</Sidebar.Trigger>
|
|
||||||
{renderMenu()}
|
|
||||||
{excalidrawAPI && (
|
|
||||||
<TTDDialogTrigger icon={<span>😀</span>}>
|
|
||||||
Text to diagram
|
|
||||||
</TTDDialogTrigger>
|
|
||||||
)}
|
|
||||||
<TTDDialog
|
|
||||||
onTextSubmit={async (_) => {
|
|
||||||
console.info("submit");
|
|
||||||
// sleep for 2s
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
throw new Error("error, go away now");
|
|
||||||
// return "dummy";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Excalidraw>
|
|
||||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||||
{comment && renderComment()}
|
{comment && renderComment()}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExcalidrawImperativeAPI } from "../types";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
|
|
||||||
const { Button, MIME_TYPES } = window.ExcalidrawLib;
|
|
||||||
const COMMENT_SVG = (
|
const COMMENT_SVG = (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -17,24 +17,28 @@ const COMMENT_SVG = (
|
|||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const CustomFooter = ({
|
const CustomFooter = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
|
excalidrawLib,
|
||||||
}: {
|
}: {
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
|
excalidrawLib: typeof TExcalidraw;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { Button, MIME_TYPES } = excalidrawLib;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onSelect={() => alert("General Kenobi!")}
|
onSelect={() => alert("General Kenobi!")}
|
||||||
className="you are a bold one"
|
style={{ marginLeft: "1rem", width: "auto" }}
|
||||||
style={{ marginLeft: "1rem" }}
|
|
||||||
title="Hello there!"
|
title="Hello there!"
|
||||||
>
|
>
|
||||||
{COMMENT_SVG}
|
Hit me
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className="custom-element"
|
className="custom-element"
|
||||||
onClick={() => {
|
onSelect={() => {
|
||||||
excalidrawAPI?.setActiveTool({
|
excalidrawAPI?.setActiveTool({
|
||||||
type: "custom",
|
type: "custom",
|
||||||
customType: "comment",
|
customType: "comment",
|
||||||
@@ -57,15 +61,10 @@ const CustomFooter = ({
|
|||||||
)}`;
|
)}`;
|
||||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||||
}}
|
}}
|
||||||
|
title="Comments!"
|
||||||
>
|
>
|
||||||
{COMMENT_SVG}
|
{COMMENT_SVG}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
|
||||||
className="custom-footer"
|
|
||||||
onClick={() => alert("This is dummy footer")}
|
|
||||||
>
|
|
||||||
custom footer
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
|
import CustomFooter from "./CustomFooter";
|
||||||
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
const MobileFooter = ({
|
||||||
|
excalidrawAPI,
|
||||||
|
excalidrawLib,
|
||||||
|
}: {
|
||||||
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
|
excalidrawLib: typeof TExcalidraw;
|
||||||
|
}) => {
|
||||||
|
const { useDevice, Footer } = excalidrawLib;
|
||||||
|
|
||||||
|
const device = useDevice();
|
||||||
|
if (device.editor.isMobile) {
|
||||||
|
return (
|
||||||
|
<Footer>
|
||||||
|
<CustomFooter
|
||||||
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
/>
|
||||||
|
</Footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
export default MobileFooter;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import "./ExampleSidebar.scss";
|
import "./ExampleSidebar.scss";
|
||||||
|
|
||||||
const React = window.React;
|
|
||||||
|
|
||||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
|
||||||
import type { FileId } from "../element/types";
|
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||||
|
|
||||||
const elements: ExcalidrawElementSkeleton[] = [
|
const elements: ExcalidrawElementSkeleton[] = [
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "examples",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@excalidraw/excalidraw": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig"
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||||
|
import type { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
|
import { AbortError } from "../../packages/excalidraw/errors";
|
||||||
|
|
||||||
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|
||||||
|
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
export type ResolvablePromise<T> = Promise<T> & {
|
||||||
|
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
};
|
||||||
|
export const resolvablePromise = <T>() => {
|
||||||
|
let resolve!: any;
|
||||||
|
let reject!: any;
|
||||||
|
const promise = new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve;
|
||||||
|
reject = _reject;
|
||||||
|
});
|
||||||
|
(promise as any).resolve = resolve;
|
||||||
|
(promise as any).reject = reject;
|
||||||
|
return promise as ResolvablePromise<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
|
||||||
|
const xd = x2 - x1;
|
||||||
|
const yd = y2 - y1;
|
||||||
|
return Math.hypot(xd, yd);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||||
|
extensions?: FILE_EXTENSION[];
|
||||||
|
description: string;
|
||||||
|
multiple?: M;
|
||||||
|
}): Promise<M extends false | undefined ? File : File[]> => {
|
||||||
|
// an unsafe TS hack, alas not much we can do AFAIK
|
||||||
|
type RetType = M extends false | undefined ? File : File[];
|
||||||
|
|
||||||
|
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||||
|
mimeTypes.push(MIME_TYPES[type]);
|
||||||
|
|
||||||
|
return mimeTypes;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
const extensions = opts.extensions?.reduce((acc, ext) => {
|
||||||
|
if (ext === "jpg") {
|
||||||
|
return acc.concat(".jpg", ".jpeg");
|
||||||
|
}
|
||||||
|
return acc.concat(`.${ext}`);
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
return _fileOpen({
|
||||||
|
description: opts.description,
|
||||||
|
extensions,
|
||||||
|
mimeTypes,
|
||||||
|
multiple: opts.multiple ?? false,
|
||||||
|
legacySetup: (resolve, reject, input) => {
|
||||||
|
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
||||||
|
const focusHandler = () => {
|
||||||
|
checkForFile();
|
||||||
|
document.addEventListener("keyup", scheduleRejection);
|
||||||
|
document.addEventListener("pointerup", scheduleRejection);
|
||||||
|
scheduleRejection();
|
||||||
|
};
|
||||||
|
const checkForFile = () => {
|
||||||
|
// this hack might not work when expecting multiple files
|
||||||
|
if (input.files?.length) {
|
||||||
|
const ret = opts.multiple ? [...input.files] : input.files[0];
|
||||||
|
resolve(ret as RetType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.addEventListener("focus", focusHandler);
|
||||||
|
});
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
checkForFile();
|
||||||
|
}, INPUT_CHANGE_INTERVAL_MS);
|
||||||
|
return (rejectPromise) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
scheduleRejection.cancel();
|
||||||
|
window.removeEventListener("focus", focusHandler);
|
||||||
|
document.removeEventListener("keyup", scheduleRejection);
|
||||||
|
document.removeEventListener("pointerup", scheduleRejection);
|
||||||
|
if (rejectPromise) {
|
||||||
|
// so that something is shown in console if we need to debug this
|
||||||
|
console.warn("Opening the file was canceled (legacy-fs).");
|
||||||
|
rejectPromise(new AbortError());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}) as Promise<RetType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debounce = <T extends any[]>(
|
||||||
|
fn: (...args: T) => void,
|
||||||
|
timeout: number,
|
||||||
|
) => {
|
||||||
|
let handle = 0;
|
||||||
|
let lastArgs: T | null = null;
|
||||||
|
const ret = (...args: T) => {
|
||||||
|
lastArgs = args;
|
||||||
|
clearTimeout(handle);
|
||||||
|
handle = window.setTimeout(() => {
|
||||||
|
lastArgs = null;
|
||||||
|
fn(...args);
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
ret.flush = () => {
|
||||||
|
clearTimeout(handle);
|
||||||
|
if (lastArgs) {
|
||||||
|
const _lastArgs = lastArgs;
|
||||||
|
lastArgs = null;
|
||||||
|
fn(..._lastArgs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ret.cancel = () => {
|
||||||
|
lastArgs = null;
|
||||||
|
clearTimeout(handle);
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withBatchedUpdates = <
|
||||||
|
TFunction extends ((event: any) => void) | (() => void),
|
||||||
|
>(
|
||||||
|
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
||||||
|
) =>
|
||||||
|
((event) => {
|
||||||
|
unstable_batchedUpdates(func as TFunction, event);
|
||||||
|
}) as TFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* barches React state updates and throttles the calls to a single call per
|
||||||
|
* animation frame
|
||||||
|
*/
|
||||||
|
export const withBatchedUpdatesThrottled = <
|
||||||
|
TFunction extends ((event: any) => void) | (() => void),
|
||||||
|
>(
|
||||||
|
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
return throttleRAF<Parameters<TFunction>>(((event) => {
|
||||||
|
unstable_batchedUpdates(func, event);
|
||||||
|
}) as TFunction);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3005) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
distDir: "build",
|
||||||
|
typescript: {
|
||||||
|
// The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed.
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
// This is needed as in pages router the code for importing types throws error as its outside next js app
|
||||||
|
transpilePackages: ["../"],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "with-nextjs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
|
||||||
|
"dev": "yarn build:workspace && next dev -p 3005",
|
||||||
|
"build": "yarn build:workspace && next build",
|
||||||
|
"start": "next start -p 3006",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@excalidraw/excalidraw": "*",
|
||||||
|
"next": "14.1",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"path2d-polyfill": "2.0.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import "../common.scss";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
// with ssr false
|
||||||
|
const ExcalidrawWithClientOnly = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
||||||
|
<h1 className="page-title">App Router</h1>
|
||||||
|
|
||||||
|
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||||
|
<ExcalidrawWithClientOnly />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1c7ed6;
|
||||||
|
font-size: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import App from "../../components/App";
|
||||||
|
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<App
|
||||||
|
appTitle={"Excalidraw with Nextjs Example"}
|
||||||
|
useCustom={(api: any, args?: any[]) => {}}
|
||||||
|
excalidrawLib={excalidrawLib}
|
||||||
|
>
|
||||||
|
<Excalidraw />
|
||||||
|
</App>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExcalidrawWrapper;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import "../common.scss";
|
||||||
|
|
||||||
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
// with ssr false
|
||||||
|
const Excalidraw = dynamic(
|
||||||
|
async () => (await import("../excalidrawWrapper")).default,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href="/">Switch to App router</a>
|
||||||
|
<h1 className="page-title">Pages Router</h1>
|
||||||
|
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||||
|
<Excalidraw />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"outputDirectory": "build"
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@excalidraw/excalidraw@workspace:^":
|
||||||
|
version "0.17.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96"
|
||||||
|
integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ==
|
||||||
|
|
||||||
|
"@next/env@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a"
|
||||||
|
integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618"
|
||||||
|
integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b"
|
||||||
|
integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21"
|
||||||
|
integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd"
|
||||||
|
integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32"
|
||||||
|
integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247"
|
||||||
|
integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3"
|
||||||
|
integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==
|
||||||
|
|
||||||
|
"@next/swc-win32-ia32-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600"
|
||||||
|
integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc@14.0.4":
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1"
|
||||||
|
integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==
|
||||||
|
|
||||||
|
"@swc/helpers@0.5.2":
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||||
|
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
"@types/node@^20":
|
||||||
|
version "20.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
|
||||||
|
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||||
|
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||||
|
|
||||||
|
"@types/react-dom@^18":
|
||||||
|
version "18.2.18"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
||||||
|
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react@*", "@types/react@^18":
|
||||||
|
version "18.2.47"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40"
|
||||||
|
integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
|
||||||
|
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
|
||||||
|
|
||||||
|
busboy@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||||
|
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||||
|
dependencies:
|
||||||
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001406:
|
||||||
|
version "1.0.30001576"
|
||||||
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4"
|
||||||
|
integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==
|
||||||
|
|
||||||
|
client-only@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
glob-to-regexp@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||||
|
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||||
|
|
||||||
|
graceful-fs@^4.1.2, graceful-fs@^4.2.11:
|
||||||
|
version "4.2.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
nanoid@^3.3.6:
|
||||||
|
version "3.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
|
next@14.0.4:
|
||||||
|
version "14.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc"
|
||||||
|
integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==
|
||||||
|
dependencies:
|
||||||
|
"@next/env" "14.0.4"
|
||||||
|
"@swc/helpers" "0.5.2"
|
||||||
|
busboy "1.6.0"
|
||||||
|
caniuse-lite "^1.0.30001406"
|
||||||
|
graceful-fs "^4.2.11"
|
||||||
|
postcss "8.4.31"
|
||||||
|
styled-jsx "5.1.1"
|
||||||
|
watchpack "2.4.0"
|
||||||
|
optionalDependencies:
|
||||||
|
"@next/swc-darwin-arm64" "14.0.4"
|
||||||
|
"@next/swc-darwin-x64" "14.0.4"
|
||||||
|
"@next/swc-linux-arm64-gnu" "14.0.4"
|
||||||
|
"@next/swc-linux-arm64-musl" "14.0.4"
|
||||||
|
"@next/swc-linux-x64-gnu" "14.0.4"
|
||||||
|
"@next/swc-linux-x64-musl" "14.0.4"
|
||||||
|
"@next/swc-win32-arm64-msvc" "14.0.4"
|
||||||
|
"@next/swc-win32-ia32-msvc" "14.0.4"
|
||||||
|
"@next/swc-win32-x64-msvc" "14.0.4"
|
||||||
|
|
||||||
|
path2d-polyfill@2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391"
|
||||||
|
integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==
|
||||||
|
|
||||||
|
picocolors@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
postcss@8.4.31:
|
||||||
|
version "8.4.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||||
|
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.6"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
react-dom@^18:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react@^18:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
scheduler@^0.23.0:
|
||||||
|
version "0.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||||
|
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
source-map-js@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
|
streamsearch@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||||
|
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||||
|
|
||||||
|
styled-jsx@5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||||
|
integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==
|
||||||
|
dependencies:
|
||||||
|
client-only "0.0.1"
|
||||||
|
|
||||||
|
tslib@^2.4.0:
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
||||||
|
typescript@^5:
|
||||||
|
version "5.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||||
|
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
|
watchpack@2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
|
dependencies:
|
||||||
|
glob-to-regexp "^0.4.1"
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
@@ -13,20 +13,20 @@
|
|||||||
window.name = "codesandbox";
|
window.name = "codesandbox";
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||||
<link rel="stylesheet" href="bundle.css" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
|
||||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
|
||||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||||
of the actual source code -->
|
of the actual source code -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as ExcalidrawLib from "/dist/browser/dev/index.js";
|
import * as ExcalidrawLib from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
console.log(ExcalidrawLib);
|
||||||
window.ExcalidrawLib = ExcalidrawLib;
|
window.ExcalidrawLib = ExcalidrawLib;
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="bundle.js"></script>
|
<script type="module" src="index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import App from "../components/App";
|
||||||
|
import React, { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ExcalidrawLib: typeof TExcalidraw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("root")!;
|
||||||
|
const root = createRoot(rootElement);
|
||||||
|
const { Excalidraw } = window.ExcalidrawLib;
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<App
|
||||||
|
appTitle={"Excalidraw Example"}
|
||||||
|
useCustom={(api: any, args?: any[]) => {}}
|
||||||
|
excalidrawLib={window.ExcalidrawLib}
|
||||||
|
>
|
||||||
|
<Excalidraw />
|
||||||
|
</App>
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "with-script-in-browser",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@excalidraw/excalidraw": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "5.0.12",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
|
||||||
|
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
|
||||||
|
"build:preview": "yarn build && vite preview --port 5002"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"outputDirectory": "example/public",
|
"outputDirectory": "dist",
|
||||||
"installCommand": "yarn install"
|
"installCommand": "yarn install"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
// open the browser
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
publicDir: "public",
|
||||||
|
});
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3"
|
||||||
|
integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==
|
||||||
|
|
||||||
|
"@esbuild/android-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220"
|
||||||
|
integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==
|
||||||
|
|
||||||
|
"@esbuild/android-arm@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c"
|
||||||
|
integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==
|
||||||
|
|
||||||
|
"@esbuild/android-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2"
|
||||||
|
integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf"
|
||||||
|
integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e"
|
||||||
|
integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a"
|
||||||
|
integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2"
|
||||||
|
integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545"
|
||||||
|
integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==
|
||||||
|
|
||||||
|
"@esbuild/linux-arm@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3"
|
||||||
|
integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4"
|
||||||
|
integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121"
|
||||||
|
integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9"
|
||||||
|
integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912"
|
||||||
|
integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916"
|
||||||
|
integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8"
|
||||||
|
integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==
|
||||||
|
|
||||||
|
"@esbuild/linux-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766"
|
||||||
|
integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d"
|
||||||
|
integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2"
|
||||||
|
integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767"
|
||||||
|
integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee"
|
||||||
|
integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c"
|
||||||
|
integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==
|
||||||
|
|
||||||
|
"@esbuild/win32-x64@0.19.11":
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04"
|
||||||
|
integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
|
||||||
|
integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a"
|
||||||
|
integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb"
|
||||||
|
integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1"
|
||||||
|
integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560"
|
||||||
|
integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114"
|
||||||
|
integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632"
|
||||||
|
integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad"
|
||||||
|
integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
|
||||||
|
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4"
|
||||||
|
integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a"
|
||||||
|
integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85"
|
||||||
|
integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.9.5":
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
|
||||||
|
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
|
||||||
|
|
||||||
|
"@types/estree@1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||||
|
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||||
|
|
||||||
|
esbuild@^0.19.3:
|
||||||
|
version "0.19.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96"
|
||||||
|
integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/aix-ppc64" "0.19.11"
|
||||||
|
"@esbuild/android-arm" "0.19.11"
|
||||||
|
"@esbuild/android-arm64" "0.19.11"
|
||||||
|
"@esbuild/android-x64" "0.19.11"
|
||||||
|
"@esbuild/darwin-arm64" "0.19.11"
|
||||||
|
"@esbuild/darwin-x64" "0.19.11"
|
||||||
|
"@esbuild/freebsd-arm64" "0.19.11"
|
||||||
|
"@esbuild/freebsd-x64" "0.19.11"
|
||||||
|
"@esbuild/linux-arm" "0.19.11"
|
||||||
|
"@esbuild/linux-arm64" "0.19.11"
|
||||||
|
"@esbuild/linux-ia32" "0.19.11"
|
||||||
|
"@esbuild/linux-loong64" "0.19.11"
|
||||||
|
"@esbuild/linux-mips64el" "0.19.11"
|
||||||
|
"@esbuild/linux-ppc64" "0.19.11"
|
||||||
|
"@esbuild/linux-riscv64" "0.19.11"
|
||||||
|
"@esbuild/linux-s390x" "0.19.11"
|
||||||
|
"@esbuild/linux-x64" "0.19.11"
|
||||||
|
"@esbuild/netbsd-x64" "0.19.11"
|
||||||
|
"@esbuild/openbsd-x64" "0.19.11"
|
||||||
|
"@esbuild/sunos-x64" "0.19.11"
|
||||||
|
"@esbuild/win32-arm64" "0.19.11"
|
||||||
|
"@esbuild/win32-ia32" "0.19.11"
|
||||||
|
"@esbuild/win32-x64" "0.19.11"
|
||||||
|
|
||||||
|
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
|
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
nanoid@^3.3.7:
|
||||||
|
version "3.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
|
picocolors@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
postcss@^8.4.32:
|
||||||
|
version "8.4.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
|
||||||
|
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.7"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
react-dom@18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react@18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
rollup@^4.2.0:
|
||||||
|
version "4.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05"
|
||||||
|
integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "1.0.5"
|
||||||
|
optionalDependencies:
|
||||||
|
"@rollup/rollup-android-arm-eabi" "4.9.5"
|
||||||
|
"@rollup/rollup-android-arm64" "4.9.5"
|
||||||
|
"@rollup/rollup-darwin-arm64" "4.9.5"
|
||||||
|
"@rollup/rollup-darwin-x64" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-arm64-musl" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-x64-gnu" "4.9.5"
|
||||||
|
"@rollup/rollup-linux-x64-musl" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-arm64-msvc" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-ia32-msvc" "4.9.5"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.9.5"
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
scheduler@^0.23.0:
|
||||||
|
version "0.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||||
|
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
source-map-js@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
|
vite@5.0.6:
|
||||||
|
version "5.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c"
|
||||||
|
integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.19.3"
|
||||||
|
postcss "^8.4.32"
|
||||||
|
rollup "^4.2.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.3"
|
||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
withBatchedUpdates,
|
|
||||||
} from "../../packages/excalidraw/utils";
|
} from "../../packages/excalidraw/utils";
|
||||||
import {
|
import {
|
||||||
CURSOR_SYNC_TIMEOUT,
|
CURSOR_SYNC_TIMEOUT,
|
||||||
@@ -37,7 +36,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getCollabServer,
|
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
@@ -83,6 +81,7 @@ import { atom, useAtom } from "jotai";
|
|||||||
import { appJotaiStore } from "../app-jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
||||||
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
|
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const collabDialogShownAtom = atom(false);
|
export const collabDialogShownAtom = atom(false);
|
||||||
@@ -452,13 +451,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const socketServerData = await getCollabServer();
|
|
||||||
|
|
||||||
this.portal.socket = this.portal.open(
|
this.portal.socket = this.portal.open(
|
||||||
socketIOClient(socketServerData.url, {
|
socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
||||||
transports: socketServerData.polling
|
transports: ["websocket", "polling"],
|
||||||
? ["websocket", "polling"]
|
|
||||||
: ["websocket"],
|
|
||||||
}),
|
}),
|
||||||
roomId,
|
roomId,
|
||||||
roomKey,
|
roomKey,
|
||||||
|
|||||||
@@ -65,35 +65,6 @@ const generateRoomId = async () => {
|
|||||||
return bytesToHexString(buffer);
|
return bytesToHexString(buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Right now the reason why we resolve connection params (url, polling...)
|
|
||||||
* from upstream is to allow changing the params immediately when needed without
|
|
||||||
* having to wait for clients to update the SW.
|
|
||||||
*
|
|
||||||
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
|
||||||
*/
|
|
||||||
export const getCollabServer = async (): Promise<{
|
|
||||||
url: string;
|
|
||||||
polling: boolean;
|
|
||||||
}> => {
|
|
||||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
|
||||||
return {
|
|
||||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
|
||||||
polling: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(
|
|
||||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
|
||||||
);
|
|
||||||
return await resp.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw new Error(t("errors.cannotResolveCollabServer"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EncryptedData = {
|
export type EncryptedData = {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
iv: Uint8Array;
|
iv: Uint8Array;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"name": "excalidraw-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": ".",
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@@ -22,17 +26,13 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"homepage": ".",
|
|
||||||
"name": "excalidraw-app",
|
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
||||||
"build:version": "node ../scripts/build-version.js",
|
"build:version": "node ../scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"install:deps": "yarn install --frozen-lockfile && yarn --cwd ../",
|
|
||||||
"start": "yarn && vite",
|
"start": "yarn && vite",
|
||||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
||||||
"build:preview": "yarn build && vite preview --port 5000"
|
"build:preview": "yarn build && vite preview --port 5000"
|
||||||
|
|||||||
@@ -20,17 +20,6 @@ Object.defineProperty(window, "crypto", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
|
|
||||||
const module = (await importActual()) as any;
|
|
||||||
return {
|
|
||||||
__esmodule: true,
|
|
||||||
...module,
|
|
||||||
getCollabServer: vi.fn(() => ({
|
|
||||||
url: /* doesn't really matter */ "http://localhost:3002",
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||||
const loadFromFirebase = async () => null;
|
const loadFromFirebase = async () => null;
|
||||||
const saveToFirebase = () => {};
|
const saveToFirebase = () => {};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/excalidraw",
|
||||||
"packages/utils"
|
"packages/utils",
|
||||||
|
"examples/excalidraw",
|
||||||
|
"examples/excalidraw/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"vite": "5.0.6",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.6.1",
|
"vite-plugin-checker": "0.6.1",
|
||||||
"vite-plugin-ejs": "1.7.0",
|
"vite-plugin-ejs": "1.7.0",
|
||||||
"vite-plugin-pwa": "0.17.4",
|
"vite-plugin-pwa": "0.17.4",
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
types
|
types
|
||||||
bundle.js
|
|
||||||
bundle.css
|
|
||||||
|
|||||||
@@ -13,10 +13,48 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add `onPointerUp` prop [#7638](https://github.com/excalidraw/excalidraw/pull/7638).
|
||||||
|
|
||||||
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
||||||
|
|
||||||
|
- Create an `ESM` build for `@excalidraw/excalidraw`. The API is in progress and subject to change before stable release. There are some changes on how the package will be consumed
|
||||||
|
|
||||||
|
#### Bundler
|
||||||
|
|
||||||
|
- CSS needs to be imported so you will need to import the css along with the excalidraw component
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `types` path is updated
|
||||||
|
|
||||||
|
Instead of importing from `@excalidraw/excalidraw/types/`, you will need to import from `@excalidraw/excalidraw/dist/excalidraw` or `@excalidraw/excalidraw/dist/utils` depending on the types you are using.
|
||||||
|
|
||||||
|
However this we will be fixing before stable release, so in case you want to try it out you will need to update the types for now.
|
||||||
|
|
||||||
|
#### Browser
|
||||||
|
|
||||||
|
- Since its `ESM` so now script type `module` can be used to load it and css needs to be loaded as well.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/@excalidraw/excalidraw@next/dist/browser/dev/index.css"
|
||||||
|
/>
|
||||||
|
<script type="module">
|
||||||
|
import * as ExcalidrawLib from "https://unpkg.com/@excalidraw/excalidraw@next/dist/browser/dev/index.js";
|
||||||
|
window.ExcalidrawLib = ExcalidrawLib;
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
|
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
|
||||||
|
|
||||||
## 0.17.1 (2023-11-28)
|
## 0.17.1 (2023-11-28)
|
||||||
@@ -233,7 +271,7 @@ define: {
|
|||||||
|
|
||||||
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
|
||||||
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||||
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||||
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
||||||
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
||||||
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||||
|
|||||||
@@ -40,8 +40,13 @@ const alignSelectedElements = (
|
|||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
|
|
||||||
const updatedElements = alignElements(selectedElements, alignment);
|
const updatedElements = alignElements(
|
||||||
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
|
alignment,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "../element/textWysiwyg";
|
} from "../element/containerCache";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
@@ -45,8 +45,9 @@ export const actionUnbindText = register({
|
|||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height, baseline } = measureText(
|
||||||
boundTextElement.originalText,
|
boundTextElement.originalText,
|
||||||
@@ -106,7 +107,10 @@ export const actionBindText = register({
|
|||||||
if (
|
if (
|
||||||
textElement &&
|
textElement &&
|
||||||
bindingContainer &&
|
bindingContainer &&
|
||||||
getBoundTextElement(bindingContainer) === null
|
getBoundTextElement(
|
||||||
|
bindingContainer,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
) === null
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ const distributeSelectedElements = (
|
|||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
const updatedElements = distributeElements(selectedElements, distribution);
|
const updatedElements = distributeElements(
|
||||||
|
selectedElements,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
distribution,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const duplicateElements = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
|
||||||
const isElementAFrameLike = isFrameLikeElement(element);
|
const isElementAFrameLike = isFrameLikeElement(element);
|
||||||
|
|
||||||
if (idsOfElementsToDuplicate.get(element.id)) {
|
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedElementsMap,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "../element/types";
|
||||||
import { resizeMultipleElements } from "../element/resizeElements";
|
import { resizeMultipleElements } from "../element/resizeElements";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getCommonBoundingBox } from "../element/bounds";
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
@@ -20,7 +25,12 @@ export const actionFlipHorizontal = register({
|
|||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "horizontal"),
|
flipSelectedElements(
|
||||||
|
elements,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
appState,
|
||||||
|
"horizontal",
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
),
|
),
|
||||||
@@ -38,7 +48,12 @@ export const actionFlipVertical = register({
|
|||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "vertical"),
|
flipSelectedElements(
|
||||||
|
elements,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
appState,
|
||||||
|
"vertical",
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
),
|
),
|
||||||
@@ -53,6 +68,7 @@ export const actionFlipVertical = register({
|
|||||||
|
|
||||||
const flipSelectedElements = (
|
const flipSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
) => {
|
) => {
|
||||||
@@ -67,6 +83,7 @@ const flipSelectedElements = (
|
|||||||
|
|
||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
flipDirection,
|
flipDirection,
|
||||||
);
|
);
|
||||||
@@ -79,15 +96,17 @@ const flipSelectedElements = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const flipElements = (
|
const flipElements = (
|
||||||
elements: NonDeleted<ExcalidrawElement>[],
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
elementsMap,
|
||||||
elements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
"nw",
|
"nw",
|
||||||
true,
|
true,
|
||||||
flipDirection === "horizontal" ? maxX : minX,
|
flipDirection === "horizontal" ? maxX : minX,
|
||||||
@@ -96,7 +115,7 @@ const flipElements = (
|
|||||||
|
|
||||||
(isBindingEnabled(appState)
|
(isBindingEnabled(appState)
|
||||||
? bindOrUnbindSelectedElements
|
? bindOrUnbindSelectedElements
|
||||||
: unbindLinearElements)(elements);
|
: unbindLinearElements)(selectedElements);
|
||||||
|
|
||||||
return elements;
|
return selectedElements;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({
|
|||||||
|
|
||||||
if (isFrameLikeElement(selectedElement)) {
|
if (isFrameLikeElement(selectedElement)) {
|
||||||
return {
|
return {
|
||||||
elements: removeAllElementsFromFrame(
|
elements: removeAllElementsFromFrame(elements, selectedElement),
|
||||||
elements,
|
|
||||||
selectedElement,
|
|
||||||
appState,
|
|
||||||
),
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
|
|||||||
@@ -105,10 +105,9 @@ export const actionGroup = register({
|
|||||||
const frameElementsMap = groupByFrameLikes(selectedElements);
|
const frameElementsMap = groupByFrameLikes(selectedElements);
|
||||||
|
|
||||||
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
||||||
nextElements = removeElementsFromFrame(
|
removeElementsFromFrame(
|
||||||
nextElements,
|
|
||||||
elementsInFrame,
|
elementsInFrame,
|
||||||
appState,
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,7 +228,7 @@ export const actionUngroup = register({
|
|||||||
nextElements,
|
nextElements,
|
||||||
getElementsInResizingFrame(nextElements, frame, appState),
|
getElementsInResizingFrame(nextElements, frame, appState),
|
||||||
frame,
|
frame,
|
||||||
appState,
|
app,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export const actionGoToCollaborator = register({
|
|||||||
isBeingFollowed={isBeingFollowed}
|
isBeingFollowed={isBeingFollowed}
|
||||||
isCurrentUser={collaborator.isCurrentUser === true}
|
isCurrentUser={collaborator.isCurrentUser === true}
|
||||||
/>
|
/>
|
||||||
{collaborator.username}
|
<div className="UserList__collaborator-name">
|
||||||
|
{collaborator.username}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="UserList__collaborator-follow-status-icon"
|
className="UserList__collaborator-follow-status-icon"
|
||||||
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
|
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppState, Primitive } from "../types";
|
import { AppClassProperties, AppState, Primitive } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -66,7 +66,6 @@ import {
|
|||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@@ -189,6 +188,7 @@ const offsetElementAfterFontResize = (
|
|||||||
const changeFontSize = (
|
const changeFontSize = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||||
) => {
|
) => {
|
||||||
@@ -206,7 +206,10 @@ const changeFontSize = (
|
|||||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||||
fontSize: newFontSize,
|
fontSize: newFontSize,
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||||
|
|
||||||
@@ -600,10 +603,10 @@ export const actionChangeOpacity = register({
|
|||||||
export const actionChangeFontSize = register({
|
export const actionChangeFontSize = register({
|
||||||
name: "changeFontSize",
|
name: "changeFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, () => value, value);
|
return changeFontSize(elements, appState, app, () => value, value);
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontSize")}</legend>
|
<legend>{t("labels.fontSize")}</legend>
|
||||||
<ButtonIconSelect
|
<ButtonIconSelect
|
||||||
@@ -641,14 +644,21 @@ export const actionChangeFontSize = register({
|
|||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
return element.fontSize;
|
return element.fontSize;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return boundTextElement.fontSize;
|
return boundTextElement.fontSize;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
(element) =>
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
) !== null,
|
||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection
|
hasSelection
|
||||||
? null
|
? null
|
||||||
@@ -663,8 +673,8 @@ export const actionChangeFontSize = register({
|
|||||||
export const actionDecreaseFontSize = register({
|
export const actionDecreaseFontSize = register({
|
||||||
name: "decreaseFontSize",
|
name: "decreaseFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, (element) =>
|
return changeFontSize(elements, appState, app, (element) =>
|
||||||
Math.round(
|
Math.round(
|
||||||
// get previous value before relative increase (doesn't work fully
|
// get previous value before relative increase (doesn't work fully
|
||||||
// due to rounding and float precision issues)
|
// due to rounding and float precision issues)
|
||||||
@@ -685,8 +695,8 @@ export const actionDecreaseFontSize = register({
|
|||||||
export const actionIncreaseFontSize = register({
|
export const actionIncreaseFontSize = register({
|
||||||
name: "increaseFontSize",
|
name: "increaseFontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, (element) =>
|
return changeFontSize(elements, appState, app, (element) =>
|
||||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -703,7 +713,7 @@ export const actionIncreaseFontSize = register({
|
|||||||
export const actionChangeFontFamily = register({
|
export const actionChangeFontFamily = register({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
@@ -717,7 +727,10 @@ export const actionChangeFontFamily = register({
|
|||||||
lineHeight: getDefaultLineHeight(value),
|
lineHeight: getDefaultLineHeight(value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +745,7 @@ export const actionChangeFontFamily = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
const options: {
|
const options: {
|
||||||
value: FontFamilyValues;
|
value: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -772,14 +785,21 @@ export const actionChangeFontFamily = register({
|
|||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
return element.fontFamily;
|
return element.fontFamily;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return boundTextElement.fontFamily;
|
return boundTextElement.fontFamily;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
(element) =>
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
) !== null,
|
||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection
|
hasSelection
|
||||||
? null
|
? null
|
||||||
@@ -795,7 +815,7 @@ export const actionChangeFontFamily = register({
|
|||||||
export const actionChangeTextAlign = register({
|
export const actionChangeTextAlign = register({
|
||||||
name: "changeTextAlign",
|
name: "changeTextAlign",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
@@ -806,7 +826,10 @@ export const actionChangeTextAlign = register({
|
|||||||
oldElement,
|
oldElement,
|
||||||
{ textAlign: value },
|
{ textAlign: value },
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,7 +844,8 @@ export const actionChangeTextAlign = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.textAlign")}</legend>
|
<legend>{t("labels.textAlign")}</legend>
|
||||||
@@ -854,14 +878,18 @@ export const actionChangeTextAlign = register({
|
|||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
return element.textAlign;
|
return element.textAlign;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return boundTextElement.textAlign;
|
return boundTextElement.textAlign;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
(element) =>
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(element, elementsMap) !== null,
|
||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection ? null : appState.currentItemTextAlign,
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
@@ -875,7 +903,7 @@ export const actionChangeTextAlign = register({
|
|||||||
export const actionChangeVerticalAlign = register({
|
export const actionChangeVerticalAlign = register({
|
||||||
name: "changeVerticalAlign",
|
name: "changeVerticalAlign",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
@@ -887,7 +915,10 @@ export const actionChangeVerticalAlign = register({
|
|||||||
{ verticalAlign: value },
|
{ verticalAlign: value },
|
||||||
);
|
);
|
||||||
|
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
app.scene.getContainerElement(oldElement),
|
||||||
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,7 +932,7 @@ export const actionChangeVerticalAlign = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<ButtonIconSelect<VerticalAlign | false>
|
<ButtonIconSelect<VerticalAlign | false>
|
||||||
@@ -933,14 +964,21 @@ export const actionChangeVerticalAlign = register({
|
|||||||
if (isTextElement(element) && element.containerId) {
|
if (isTextElement(element) && element.containerId) {
|
||||||
return element.verticalAlign;
|
return element.verticalAlign;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return boundTextElement.verticalAlign;
|
return boundTextElement.verticalAlign;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
(element) =>
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
) !== null,
|
||||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ export let copiedStyles: string = "{}";
|
|||||||
export const actionCopyStyles = register({
|
export const actionCopyStyles = register({
|
||||||
name: "copyStyles",
|
name: "copyStyles",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, formData, app) => {
|
||||||
const elementsCopied = [];
|
const elementsCopied = [];
|
||||||
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
||||||
elementsCopied.push(element);
|
elementsCopied.push(element);
|
||||||
if (element && hasBoundTextElement(element)) {
|
if (element && hasBoundTextElement(element)) {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
elementsCopied.push(boundTextElement);
|
elementsCopied.push(boundTextElement);
|
||||||
}
|
}
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -59,7 +62,7 @@ export const actionCopyStyles = register({
|
|||||||
export const actionPasteStyles = register({
|
export const actionPasteStyles = register({
|
||||||
name: "pasteStyles",
|
name: "pasteStyles",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, formData, app) => {
|
||||||
const elementsCopied = JSON.parse(copiedStyles);
|
const elementsCopied = JSON.parse(copiedStyles);
|
||||||
const pastedElement = elementsCopied[0];
|
const pastedElement = elementsCopied[0];
|
||||||
const boundTextElement = elementsCopied[1];
|
const boundTextElement = elementsCopied[1];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
@@ -10,10 +10,13 @@ export interface Alignment {
|
|||||||
|
|
||||||
export const alignElements = (
|
export const alignElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
return groups.flatMap((group) => {
|
return groups.flatMap((group) => {
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
|
||||||
|
import { AnimationFrameHandler } from "./animation-frame-handler";
|
||||||
|
import { AppState } from "./types";
|
||||||
|
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
|
||||||
|
import type App from "./components/App";
|
||||||
|
import { SVG_NS } from "./constants";
|
||||||
|
|
||||||
|
export interface Trail {
|
||||||
|
start(container: SVGSVGElement): void;
|
||||||
|
stop(): void;
|
||||||
|
|
||||||
|
startPath(x: number, y: number): void;
|
||||||
|
addPointToPath(x: number, y: number): void;
|
||||||
|
endPath(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimatedTrailOptions {
|
||||||
|
fill: (trail: AnimatedTrail) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnimatedTrail implements Trail {
|
||||||
|
private currentTrail?: LaserPointer;
|
||||||
|
private pastTrails: LaserPointer[] = [];
|
||||||
|
|
||||||
|
private container?: SVGSVGElement;
|
||||||
|
private trailElement: SVGPathElement;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private animationFrameHandler: AnimationFrameHandler,
|
||||||
|
private app: App,
|
||||||
|
private options: Partial<LaserPointerOptions> &
|
||||||
|
Partial<AnimatedTrailOptions>,
|
||||||
|
) {
|
||||||
|
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
||||||
|
|
||||||
|
this.trailElement = document.createElementNS(SVG_NS, "path");
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasCurrentTrail() {
|
||||||
|
return !!this.currentTrail;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLastPoint(x: number, y: number) {
|
||||||
|
if (this.currentTrail) {
|
||||||
|
const len = this.currentTrail.originalPoints.length;
|
||||||
|
return (
|
||||||
|
this.currentTrail.originalPoints[len - 1][0] === x &&
|
||||||
|
this.currentTrail.originalPoints[len - 1][1] === y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(container?: SVGSVGElement) {
|
||||||
|
if (container) {
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.trailElement.parentNode !== this.container && this.container) {
|
||||||
|
this.container.appendChild(this.trailElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationFrameHandler.start(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.animationFrameHandler.stop(this);
|
||||||
|
|
||||||
|
if (this.trailElement.parentNode === this.container) {
|
||||||
|
this.container?.removeChild(this.trailElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startPath(x: number, y: number) {
|
||||||
|
this.currentTrail = new LaserPointer(this.options);
|
||||||
|
|
||||||
|
this.currentTrail.addPoint([x, y, performance.now()]);
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
addPointToPath(x: number, y: number) {
|
||||||
|
if (this.currentTrail) {
|
||||||
|
this.currentTrail.addPoint([x, y, performance.now()]);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endPath() {
|
||||||
|
if (this.currentTrail) {
|
||||||
|
this.currentTrail.close();
|
||||||
|
this.currentTrail.options.keepHead = false;
|
||||||
|
this.pastTrails.push(this.currentTrail);
|
||||||
|
this.currentTrail = undefined;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFrame() {
|
||||||
|
const paths: string[] = [];
|
||||||
|
|
||||||
|
for (const trail of this.pastTrails) {
|
||||||
|
paths.push(this.drawTrail(trail, this.app.state));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentTrail) {
|
||||||
|
const currentPath = this.drawTrail(this.currentTrail, this.app.state);
|
||||||
|
|
||||||
|
paths.push(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pastTrails = this.pastTrails.filter((trail) => {
|
||||||
|
return trail.getStrokeOutline().length !== 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paths.length === 0) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgPaths = paths.join(" ").trim();
|
||||||
|
|
||||||
|
this.trailElement.setAttribute("d", svgPaths);
|
||||||
|
this.trailElement.setAttribute(
|
||||||
|
"fill",
|
||||||
|
(this.options.fill ?? (() => "black"))(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawTrail(trail: LaserPointer, state: AppState): string {
|
||||||
|
const stroke = trail
|
||||||
|
.getStrokeOutline(trail.options.size / state.zoom.value)
|
||||||
|
.map(([x, y]) => {
|
||||||
|
const result = sceneCoordsToViewportCoords(
|
||||||
|
{ sceneX: x, sceneY: y },
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [result.x, result.y];
|
||||||
|
});
|
||||||
|
|
||||||
|
return getSvgPathFromStroke(stroke, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
export type AnimationCallback = (timestamp: number) => void | boolean;
|
||||||
|
|
||||||
|
export type AnimationTarget = {
|
||||||
|
callback: AnimationCallback;
|
||||||
|
stopped: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AnimationFrameHandler {
|
||||||
|
private targets = new WeakMap<object, AnimationTarget>();
|
||||||
|
private rafIds = new WeakMap<object, number>();
|
||||||
|
|
||||||
|
register(key: object, callback: AnimationCallback) {
|
||||||
|
this.targets.set(key, { callback, stopped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
start(key: object) {
|
||||||
|
const target = this.targets.get(key);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rafIds.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.targets.set(key, { ...target, stopped: false });
|
||||||
|
this.scheduleFrame(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(key: object) {
|
||||||
|
const target = this.targets.get(key);
|
||||||
|
if (target && !target.stopped) {
|
||||||
|
this.targets.set(key, { ...target, stopped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cancelFrame(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructFrame(key: object): FrameRequestCallback {
|
||||||
|
return (timestamp: number) => {
|
||||||
|
const target = this.targets.get(key);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldAbort = this.onFrame(target, timestamp);
|
||||||
|
|
||||||
|
if (!target.stopped && !shouldAbort) {
|
||||||
|
this.scheduleFrame(key);
|
||||||
|
} else {
|
||||||
|
this.cancelFrame(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleFrame(key: object) {
|
||||||
|
const rafId = requestAnimationFrame(this.constructFrame(key));
|
||||||
|
|
||||||
|
this.rafIds.set(key, rafId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelFrame(key: object) {
|
||||||
|
if (this.rafIds.has(key)) {
|
||||||
|
const rafId = this.rafIds.get(key)!;
|
||||||
|
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rafIds.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFrame(target: AnimationTarget, timestamp: number): boolean {
|
||||||
|
const shouldAbort = target.callback(timestamp);
|
||||||
|
|
||||||
|
return shouldAbort ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import {
|
||||||
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
|
ExcalidrawElementType,
|
||||||
|
NonDeletedElementsMap,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
import {
|
import {
|
||||||
@@ -44,17 +47,14 @@ import { useTunnels } from "../context/tunnels";
|
|||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
elementsMap,
|
||||||
renderAction,
|
renderAction,
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
let isSingleElementBoundContainer = false;
|
let isSingleElementBoundContainer = false;
|
||||||
if (
|
if (
|
||||||
@@ -137,12 +137,12 @@ export const SelectedShapeActions = ({
|
|||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
suppportsHorizontalAlign(targetElements)) &&
|
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldAllowVerticalAlign(targetElements) &&
|
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||||
renderAction("changeVerticalAlign")}
|
renderAction("changeVerticalAlign")}
|
||||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
|
|||||||
@@ -16,25 +16,20 @@ const FollowMode = ({
|
|||||||
onDisconnect,
|
onDisconnect,
|
||||||
}: FollowModeProps) => {
|
}: FollowModeProps) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div className="follow-mode" style={{ width, height }}>
|
||||||
<div className="follow-mode" style={{ width, height }}>
|
<div className="follow-mode__badge">
|
||||||
<div className="follow-mode__badge">
|
<div className="follow-mode__badge__label">
|
||||||
<div className="follow-mode__badge__label">
|
Following{" "}
|
||||||
Following{" "}
|
<span
|
||||||
<span
|
className="follow-mode__badge__username"
|
||||||
className="follow-mode__badge__username"
|
title={userToFollow.username}
|
||||||
title={userToFollow.username}
|
|
||||||
>
|
|
||||||
{userToFollow.username}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onDisconnect}
|
|
||||||
className="follow-mode__disconnect-btn"
|
|
||||||
>
|
>
|
||||||
{CloseIcon}
|
{userToFollow.username}
|
||||||
</button>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
||||||
|
{CloseIcon}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "../ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButtonSize } from "../ToolButton";
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
import { laserPointerToolIcon } from "../icons";
|
import { laserPointerToolIcon } from "./icons";
|
||||||
|
|
||||||
type LaserPointerIconProps = {
|
type LaserPointerIconProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import { LaserPointer } from "@excalidraw/laser-pointer";
|
|
||||||
|
|
||||||
import { sceneCoordsToViewportCoords } from "../../utils";
|
|
||||||
import App from "../App";
|
|
||||||
import { getClientColor } from "../../clients";
|
|
||||||
import { SocketId } from "../../types";
|
|
||||||
|
|
||||||
// decay time in milliseconds
|
|
||||||
const DECAY_TIME = 1000;
|
|
||||||
// length of line in points before it starts decaying
|
|
||||||
const DECAY_LENGTH = 50;
|
|
||||||
|
|
||||||
const average = (a: number, b: number) => (a + b) / 2;
|
|
||||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
|
||||||
const len = points.length;
|
|
||||||
|
|
||||||
if (len < 4) {
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = points[0];
|
|
||||||
let b = points[1];
|
|
||||||
const c = points[2];
|
|
||||||
|
|
||||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
|
|
||||||
2,
|
|
||||||
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
|
|
||||||
b[1],
|
|
||||||
c[1],
|
|
||||||
).toFixed(2)} T`;
|
|
||||||
|
|
||||||
for (let i = 2, max = len - 1; i < max; i++) {
|
|
||||||
a = points[i];
|
|
||||||
b = points[i + 1];
|
|
||||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
|
|
||||||
2,
|
|
||||||
)} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closed) {
|
|
||||||
result += "Z";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
LPM: LaserPathManager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function easeOutCubic(t: number) {
|
|
||||||
return 1 - Math.pow(1 - t, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function instantiateCollabolatorState(): CollabolatorState {
|
|
||||||
return {
|
|
||||||
currentPath: undefined,
|
|
||||||
finishedPaths: [],
|
|
||||||
lastPoint: [-10000, -10000],
|
|
||||||
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function instantiatePath() {
|
|
||||||
LaserPointer.constants.cornerDetectionMaxAngle = 70;
|
|
||||||
|
|
||||||
return new LaserPointer({
|
|
||||||
simplify: 0,
|
|
||||||
streamline: 0.4,
|
|
||||||
sizeMapping: (c) => {
|
|
||||||
const pt = DECAY_TIME;
|
|
||||||
const pl = DECAY_LENGTH;
|
|
||||||
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
|
|
||||||
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
|
|
||||||
|
|
||||||
return Math.min(easeOutCubic(l), easeOutCubic(t));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollabolatorState = {
|
|
||||||
currentPath: LaserPointer | undefined;
|
|
||||||
finishedPaths: LaserPointer[];
|
|
||||||
lastPoint: [number, number];
|
|
||||||
svg: SVGPathElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LaserPathManager {
|
|
||||||
private ownState: CollabolatorState;
|
|
||||||
private collaboratorsState: Map<SocketId, CollabolatorState> = new Map();
|
|
||||||
|
|
||||||
private rafId: number | undefined;
|
|
||||||
private isDrawing = false;
|
|
||||||
private container: SVGSVGElement | undefined;
|
|
||||||
|
|
||||||
constructor(private app: App) {
|
|
||||||
this.ownState = instantiateCollabolatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stop();
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.ownState = instantiateCollabolatorState();
|
|
||||||
this.collaboratorsState = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
startPath(x: number, y: number) {
|
|
||||||
this.ownState.currentPath = instantiatePath();
|
|
||||||
this.ownState.currentPath.addPoint([x, y, performance.now()]);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPointToPath(x: number, y: number) {
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endPath() {
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
this.ownState.currentPath.close();
|
|
||||||
this.ownState.finishedPaths.push(this.ownState.currentPath);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updatePath(state: CollabolatorState) {
|
|
||||||
this.isDrawing = true;
|
|
||||||
|
|
||||||
if (!this.isRunning) {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isRunning = false;
|
|
||||||
|
|
||||||
start(svg?: SVGSVGElement) {
|
|
||||||
if (svg) {
|
|
||||||
this.container = svg;
|
|
||||||
this.container.appendChild(this.ownState.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stop();
|
|
||||||
this.isRunning = true;
|
|
||||||
this.loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.isRunning = false;
|
|
||||||
if (this.rafId) {
|
|
||||||
cancelAnimationFrame(this.rafId);
|
|
||||||
}
|
|
||||||
this.rafId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
loop() {
|
|
||||||
this.rafId = requestAnimationFrame(this.loop.bind(this));
|
|
||||||
|
|
||||||
this.updateCollabolatorsState();
|
|
||||||
|
|
||||||
if (this.isDrawing) {
|
|
||||||
this.update();
|
|
||||||
} else {
|
|
||||||
this.isRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(path: LaserPointer) {
|
|
||||||
const stroke = path
|
|
||||||
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
|
|
||||||
.map(([x, y]) => {
|
|
||||||
const result = sceneCoordsToViewportCoords(
|
|
||||||
{ sceneX: x, sceneY: y },
|
|
||||||
this.app.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [result.x, result.y];
|
|
||||||
});
|
|
||||||
|
|
||||||
return getSvgPathFromStroke(stroke, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCollabolatorsState() {
|
|
||||||
if (!this.container || !this.app.state.collaborators.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
|
|
||||||
if (!this.collaboratorsState.has(key)) {
|
|
||||||
const state = instantiateCollabolatorState();
|
|
||||||
this.container.appendChild(state.svg);
|
|
||||||
this.collaboratorsState.set(key, state);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = this.collaboratorsState.get(key)!;
|
|
||||||
|
|
||||||
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
|
|
||||||
if (collabolator.button === "down" && state.currentPath === undefined) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath = instantiatePath();
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collabolator.button === "down" && state.currentPath !== undefined) {
|
|
||||||
if (
|
|
||||||
collabolator.pointer.x !== state.lastPoint[0] ||
|
|
||||||
collabolator.pointer.y !== state.lastPoint[1]
|
|
||||||
) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collabolator.button === "up" && state.currentPath !== undefined) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
state.currentPath.close();
|
|
||||||
|
|
||||||
state.finishedPaths.push(state.currentPath);
|
|
||||||
state.currentPath = undefined;
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let somePathsExist = false;
|
|
||||||
|
|
||||||
for (const [key, state] of this.collaboratorsState.entries()) {
|
|
||||||
if (!this.app.state.collaborators.has(key)) {
|
|
||||||
state.svg.remove();
|
|
||||||
this.collaboratorsState.delete(key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.finishedPaths = state.finishedPaths.filter((path) => {
|
|
||||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
||||||
|
|
||||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
|
|
||||||
|
|
||||||
if (state.currentPath) {
|
|
||||||
paths += ` ${this.draw(state.currentPath)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paths.trim()) {
|
|
||||||
somePathsExist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.svg.setAttribute("d", paths);
|
|
||||||
state.svg.setAttribute("fill", getClientColor(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
|
|
||||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
||||||
|
|
||||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paths = this.ownState.finishedPaths
|
|
||||||
.map((path) => this.draw(path))
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
paths += ` ${this.draw(this.ownState.currentPath)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
paths = paths.trim();
|
|
||||||
|
|
||||||
if (paths) {
|
|
||||||
somePathsExist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ownState.svg.setAttribute("d", paths);
|
|
||||||
this.ownState.svg.setAttribute("fill", "red");
|
|
||||||
|
|
||||||
if (!somePathsExist) {
|
|
||||||
this.isDrawing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { LaserPathManager } from "./LaserPathManager";
|
|
||||||
import "./LaserToolOverlay.scss";
|
|
||||||
|
|
||||||
type LaserToolOverlayProps = {
|
|
||||||
manager: LaserPathManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
|
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (svgRef.current) {
|
|
||||||
manager.start(svgRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
manager.stop();
|
|
||||||
};
|
|
||||||
}, [manager]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="LaserToolOverlay">
|
|
||||||
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -60,7 +60,7 @@ import "./Toolbar.scss";
|
|||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
|
import { LaserPointerButton } from "./LaserPointerButton";
|
||||||
import { MagicSettings } from "./MagicSettings";
|
import { MagicSettings } from "./MagicSettings";
|
||||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export const MobileMenu = ({
|
|||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.LaserToolOverlay {
|
.SVGLayer {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
.LaserToolOverlayCanvas {
|
& svg {
|
||||||
image-rendering: auto;
|
image-rendering: auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { Trail } from "../animated-trail";
|
||||||
|
|
||||||
|
import "./SVGLayer.scss";
|
||||||
|
|
||||||
|
type SVGLayerProps = {
|
||||||
|
trails: Trail[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SVGLayer = ({ trails }: SVGLayerProps) => {
|
||||||
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (svgRef.current) {
|
||||||
|
for (const trail of trails) {
|
||||||
|
trail.start(svgRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const trail of trails) {
|
||||||
|
trail.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, trails);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="SVGLayer">
|
||||||
|
<svg ref={svgRef} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
.default-sidebar-trigger .sidebar-trigger__label {
|
.default-sidebar-trigger .sidebar-trigger__label {
|
||||||
display: block;
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
|
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
|
||||||
|
|||||||
@@ -51,6 +51,12 @@
|
|||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.UserList__collaborator-name {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.UserList__collaborator-follow-status-icon {
|
.UserList__collaborator-follow-status-icon {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { renderInteractiveScene } from "../../renderer/renderScene";
|
import { renderInteractiveScene } from "../../renderer/renderScene";
|
||||||
import {
|
import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
|
||||||
isRenderThrottlingEnabled,
|
|
||||||
isShallowEqual,
|
|
||||||
sceneCoordsToViewportCoords,
|
|
||||||
} from "../../utils";
|
|
||||||
import { CURSOR_TYPE } from "../../constants";
|
import { CURSOR_TYPE } from "../../constants";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import type { DOMAttributes } from "react";
|
import type { DOMAttributes } from "react";
|
||||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
|
RenderableElementsMap,
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
} from "../../scene/types";
|
} from "../../scene/types";
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||||
|
|
||||||
type InteractiveCanvasProps = {
|
type InteractiveCanvasProps = {
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
versionNonce: number | undefined;
|
||||||
@@ -116,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||||||
renderInteractiveScene(
|
renderInteractiveScene(
|
||||||
{
|
{
|
||||||
canvas: props.canvas,
|
canvas: props.canvas,
|
||||||
elements: props.elements,
|
elementsMap: props.elementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
selectedElements: props.selectedElements,
|
selectedElements: props.selectedElements,
|
||||||
scale: window.devicePixelRatio,
|
scale: window.devicePixelRatio,
|
||||||
@@ -204,10 +202,10 @@ const areEqual = (
|
|||||||
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on element arrays because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elements !== nextProps.elements ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
prevProps.selectedElements !== nextProps.selectedElements
|
prevProps.selectedElements !== nextProps.selectedElements
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { renderStaticScene } from "../../renderer/renderScene";
|
import { renderStaticScene } from "../../renderer/renderScene";
|
||||||
import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils";
|
import { isShallowEqual } from "../../utils";
|
||||||
import type { AppState, StaticCanvasAppState } from "../../types";
|
import type { AppState, StaticCanvasAppState } from "../../types";
|
||||||
import type { StaticCanvasRenderConfig } from "../../scene/types";
|
import type {
|
||||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
RenderableElementsMap,
|
||||||
|
StaticCanvasRenderConfig,
|
||||||
|
} from "../../scene/types";
|
||||||
|
import type {
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "../../element/types";
|
||||||
|
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||||
|
|
||||||
type StaticCanvasProps = {
|
type StaticCanvasProps = {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
rc: RoughCanvas;
|
rc: RoughCanvas;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsMap: RenderableElementsMap;
|
||||||
|
allElementsMap: NonDeletedSceneElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
versionNonce: number | undefined;
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
@@ -62,7 +70,8 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
|||||||
canvas,
|
canvas,
|
||||||
rc: props.rc,
|
rc: props.rc,
|
||||||
scale: props.scale,
|
scale: props.scale,
|
||||||
elements: props.elements,
|
elementsMap: props.elementsMap,
|
||||||
|
allElementsMap: props.allElementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
appState: props.appState,
|
appState: props.appState,
|
||||||
renderConfig: props.renderConfig,
|
renderConfig: props.renderConfig,
|
||||||
@@ -105,10 +114,10 @@ const areEqual = (
|
|||||||
if (
|
if (
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on element arrays because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elements !== nextProps.elements ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements
|
prevProps.visibleElements !== nextProps.visibleElements
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -486,10 +486,11 @@ export const DiscordIcon = createIcon(
|
|||||||
modifiedTablerIconProps,
|
modifiedTablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TwitterIcon = createIcon(
|
export const XBrandIcon = createIcon(
|
||||||
<g strokeWidth="1.25">
|
<g strokeWidth="1.25">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"></path>
|
<path d="M4 4l11.733 16h4.267l-11.733 -16z" />
|
||||||
|
<path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />
|
||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
usersIcon,
|
usersIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
import { GithubIcon, DiscordIcon, XBrandIcon } from "../icons";
|
||||||
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||||
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
||||||
import {
|
import {
|
||||||
@@ -241,31 +241,35 @@ export const Export = () => {
|
|||||||
};
|
};
|
||||||
Export.displayName = "Export";
|
Export.displayName = "Export";
|
||||||
|
|
||||||
export const Socials = () => (
|
export const Socials = () => {
|
||||||
<>
|
const { t } = useI18n();
|
||||||
<DropdownMenuItemLink
|
|
||||||
icon={GithubIcon}
|
return (
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
<>
|
||||||
aria-label="GitHub"
|
<DropdownMenuItemLink
|
||||||
>
|
icon={GithubIcon}
|
||||||
GitHub
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
</DropdownMenuItemLink>
|
aria-label="GitHub"
|
||||||
<DropdownMenuItemLink
|
>
|
||||||
icon={DiscordIcon}
|
GitHub
|
||||||
href="https://discord.gg/UexuTaE"
|
</DropdownMenuItemLink>
|
||||||
aria-label="Discord"
|
<DropdownMenuItemLink
|
||||||
>
|
icon={XBrandIcon}
|
||||||
Discord
|
href="https://x.com/excalidraw"
|
||||||
</DropdownMenuItemLink>
|
aria-label="X"
|
||||||
<DropdownMenuItemLink
|
>
|
||||||
icon={TwitterIcon}
|
{t("labels.followUs")}
|
||||||
href="https://twitter.com/excalidraw"
|
</DropdownMenuItemLink>
|
||||||
aria-label="Twitter"
|
<DropdownMenuItemLink
|
||||||
>
|
icon={DiscordIcon}
|
||||||
Twitter
|
href="https://discord.gg/UexuTaE"
|
||||||
</DropdownMenuItemLink>
|
aria-label="Discord"
|
||||||
</>
|
>
|
||||||
);
|
{t("labels.discordChat")}
|
||||||
|
</DropdownMenuItemLink>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Socials.displayName = "Socials";
|
Socials.displayName = "Socials";
|
||||||
|
|
||||||
export const LiveCollaborationTrigger = ({
|
export const LiveCollaborationTrigger = ({
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import cssVariables from "./css/variables.module.scss";
|
|||||||
import { AppProps } from "./types";
|
import { AppProps } from "./types";
|
||||||
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
import { ExcalidrawElement, FontFamilyValues } from "./element/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);
|
||||||
export const isWindows = /^Win/.test(navigator.platform);
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||||
@@ -143,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
|||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
|
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||||
|
|
||||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { elementsOverlappingBBox } from "../../utils/export";
|
|
||||||
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
@@ -20,6 +19,7 @@ import { cloneJSON } from "../utils";
|
|||||||
import { canvasToBlob } from "./blob";
|
import { canvasToBlob } from "./blob";
|
||||||
import { fileSave, FileSystemHandle } from "./filesystem";
|
import { fileSave, FileSystemHandle } from "./filesystem";
|
||||||
import { serializeAsJSON } from "./json";
|
import { serializeAsJSON } from "./json";
|
||||||
|
import { getElementsOverlappingFrame } from "../frame";
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { loadFromJSON, saveAsJSON } from "./json";
|
export { loadFromJSON, saveAsJSON } from "./json";
|
||||||
@@ -56,11 +56,7 @@ export const prepareElementsForExport = (
|
|||||||
isFrameLikeElement(exportedElements[0])
|
isFrameLikeElement(exportedElements[0])
|
||||||
) {
|
) {
|
||||||
exportingFrame = exportedElements[0];
|
exportingFrame = exportedElements[0];
|
||||||
exportedElements = elementsOverlappingBBox({
|
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||||
elements,
|
|
||||||
bounds: exportingFrame,
|
|
||||||
type: "overlap",
|
|
||||||
});
|
|
||||||
} else if (exportedElements.length > 1) {
|
} else if (exportedElements.length > 1) {
|
||||||
exportedElements = getSelectedElements(
|
exportedElements = getSelectedElements(
|
||||||
elements,
|
elements,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { arrayToMap } from "../utils";
|
|||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
@@ -179,7 +180,6 @@ const restoreElementWithProperties = <
|
|||||||
|
|
||||||
const restoreElement = (
|
const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
refreshDimensions = false,
|
|
||||||
): typeof element | null => {
|
): typeof element | null => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -232,10 +232,6 @@ const restoreElement = (
|
|||||||
element = bumpVersion(element);
|
element = bumpVersion(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refreshDimensions) {
|
|
||||||
element = { ...element, ...refreshTextDimensions(element) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
@@ -295,11 +291,8 @@ const restoreElement = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
return restoreElementWithProperties(element, {});
|
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {});
|
||||||
validated: null,
|
|
||||||
});
|
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
case "frame":
|
case "frame":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
@@ -429,10 +422,7 @@ export const restoreElements = (
|
|||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
||||||
element,
|
|
||||||
opts?.refreshDimensions,
|
|
||||||
);
|
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
const localElement = localElementsMap?.get(element.id);
|
const localElement = localElementsMap?.get(element.id);
|
||||||
if (localElement && localElement.version > migratedElement.version) {
|
if (localElement && localElement.version > migratedElement.version) {
|
||||||
@@ -465,6 +455,16 @@ export const restoreElements = (
|
|||||||
} else if (element.boundElements) {
|
} else if (element.boundElements) {
|
||||||
repairContainerElement(element, restoredElementsMap);
|
repairContainerElement(element, restoredElementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.refreshDimensions && isTextElement(element)) {
|
||||||
|
Object.assign(
|
||||||
|
element,
|
||||||
|
refreshTextDimensions(
|
||||||
|
element,
|
||||||
|
getContainerElement(element, restoredElementsMap),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoredElements;
|
return restoredElements;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
normalizeText,
|
normalizeText,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -42,7 +43,7 @@ import {
|
|||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import { MarkOptional } from "../utility-types";
|
||||||
import { assertNever, cloneJSON, getFontString } from "../utils";
|
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
|
|
||||||
@@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100;
|
|||||||
const bindTextToContainer = (
|
const bindTextToContainer = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const textElement: ExcalidrawTextElement = newTextElement({
|
const textElement: ExcalidrawTextElement = newTextElement({
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -623,6 +625,7 @@ export const convertToExcalidrawElements = (
|
|||||||
let [container, text] = bindTextToContainer(
|
let [container, text] = bindTextToContainer(
|
||||||
excalidrawElement,
|
excalidrawElement,
|
||||||
element?.label,
|
element?.label,
|
||||||
|
arrayToMap(elementStore.getElements()),
|
||||||
);
|
);
|
||||||
elementStore.add(container);
|
elementStore.add(container);
|
||||||
elementStore.add(text);
|
elementStore.add(text);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
import { getCommonBoundingBox } from "./element/bounds";
|
||||||
|
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||||
|
|
||||||
export interface Distribution {
|
export interface Distribution {
|
||||||
space: "between";
|
space: "between";
|
||||||
@@ -10,6 +10,7 @@ export interface Distribution {
|
|||||||
|
|
||||||
export const distributeElements = (
|
export const distributeElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
distribution: Distribution,
|
distribution: Distribution,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const [start, mid, end, extent] =
|
const [start, mid, end, extent] =
|
||||||
@@ -18,7 +19,7 @@ export const distributeElements = (
|
|||||||
: (["minY", "midY", "maxY", "height"] as const);
|
: (["minY", "midY", "maxY", "height"] as const);
|
||||||
|
|
||||||
const bounds = getCommonBoundingBox(selectedElements);
|
const bounds = getCommonBoundingBox(selectedElements);
|
||||||
const groups = getMaximumGroups(selectedElements)
|
const groups = getMaximumGroups(selectedElements, elementsMap)
|
||||||
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
||||||
.sort((a, b) => a[1][mid] - b[1][mid]);
|
.sort((a, b) => a[1][mid] - b[1][mid]);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import "./Hyperlink.scss";
|
|||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
||||||
import { isEmbeddableElement } from "./typeChecks";
|
import { isEmbeddableElement } from "./typeChecks";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@@ -64,6 +63,7 @@ export const Hyperlink = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
setToast,
|
setToast,
|
||||||
|
updateEmbedValidationStatus,
|
||||||
}: {
|
}: {
|
||||||
element: NonDeletedExcalidrawElement;
|
element: NonDeletedExcalidrawElement;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
@@ -71,6 +71,10 @@ export const Hyperlink = ({
|
|||||||
setToast: (
|
setToast: (
|
||||||
toast: { message: string; closable?: boolean; duration?: number } | null,
|
toast: { message: string; closable?: boolean; duration?: number } | null,
|
||||||
) => void;
|
) => void;
|
||||||
|
updateEmbedValidationStatus: (
|
||||||
|
element: ExcalidrawEmbeddableElement,
|
||||||
|
status: boolean,
|
||||||
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
@@ -98,9 +102,9 @@ export const Hyperlink = ({
|
|||||||
}
|
}
|
||||||
if (!link) {
|
if (!link) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
validated: false,
|
|
||||||
link: null,
|
link: null,
|
||||||
});
|
});
|
||||||
|
updateEmbedValidationStatus(element, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,15 +114,17 @@ export const Hyperlink = ({
|
|||||||
}
|
}
|
||||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
validated: false,
|
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
ShapeCache.delete(element);
|
updateEmbedValidationStatus(element, false);
|
||||||
} else {
|
} else {
|
||||||
const { width, height } = element;
|
const { width, height } = element;
|
||||||
const embedLink = getEmbedLink(link);
|
const embedLink = getEmbedLink(link);
|
||||||
if (embedLink?.warning) {
|
if (embedLink?.error instanceof URIError) {
|
||||||
setToast({ message: embedLink.warning, closable: true });
|
setToast({
|
||||||
|
message: t("toast.unrecognizedLinkFormat"),
|
||||||
|
closable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const ar = embedLink
|
const ar = embedLink
|
||||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||||
@@ -142,10 +148,9 @@ export const Hyperlink = ({
|
|||||||
: height,
|
: height,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
validated: true,
|
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
ShapeCache.delete(element);
|
updateEmbedValidationStatus(element, true);
|
||||||
if (embeddableLinkCache.has(element.id)) {
|
if (embeddableLinkCache.has(element.id)) {
|
||||||
embeddableLinkCache.delete(element.id);
|
embeddableLinkCache.delete(element.id);
|
||||||
}
|
}
|
||||||
@@ -159,6 +164,7 @@ export const Hyperlink = ({
|
|||||||
appProps.validateEmbeddable,
|
appProps.validateEmbeddable,
|
||||||
appState.activeEmbeddable,
|
appState.activeEmbeddable,
|
||||||
setAppState,
|
setAppState,
|
||||||
|
updateEmbedValidationStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
@@ -321,9 +321,9 @@ export const updateBoundElements = (
|
|||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
const scene = Scene.getScene(changedElement)!;
|
||||||
getNonDeletedElements(
|
getNonDeletedElements(
|
||||||
Scene.getScene(changedElement)!,
|
scene,
|
||||||
boundLinearElements.map((el) => el.id),
|
boundLinearElements.map((el) => el.id),
|
||||||
).forEach((element) => {
|
).forEach((element) => {
|
||||||
if (!isLinearElement(element)) {
|
if (!isLinearElement(element)) {
|
||||||
@@ -362,9 +362,12 @@ export const updateBoundElements = (
|
|||||||
endBinding,
|
endBinding,
|
||||||
changedElement as ExcalidrawBindableElement,
|
changedElement as ExcalidrawBindableElement,
|
||||||
);
|
);
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
if (boundText) {
|
if (boundText) {
|
||||||
handleBindTextResize(element, false);
|
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ElementsMapOrArray,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { distance2d, rotate, rotatePoint } from "../math";
|
import { distance2d, rotate, rotatePoint } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
@@ -73,13 +75,16 @@ export class ElementBounds {
|
|||||||
) {
|
) {
|
||||||
return cachedBounds.bounds;
|
return cachedBounds.bounds;
|
||||||
}
|
}
|
||||||
|
const scene = Scene.getScene(element);
|
||||||
const bounds = ElementBounds.calculateBounds(element);
|
const bounds = ElementBounds.calculateBounds(
|
||||||
|
element,
|
||||||
|
scene?.getNonDeletedElementsMap() || new Map(),
|
||||||
|
);
|
||||||
|
|
||||||
// hack to ensure that downstream checks could retrieve element Scene
|
// hack to ensure that downstream checks could retrieve element Scene
|
||||||
// so as to have correctly calculated bounds
|
// so as to have correctly calculated bounds
|
||||||
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
|
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
|
||||||
const shouldCache = Scene.getScene(element);
|
const shouldCache = !!scene;
|
||||||
|
|
||||||
if (shouldCache) {
|
if (shouldCache) {
|
||||||
ElementBounds.boundsCache.set(element, {
|
ElementBounds.boundsCache.set(element, {
|
||||||
@@ -91,7 +96,10 @@ export class ElementBounds {
|
|||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
private static calculateBounds(
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): Bounds {
|
||||||
let bounds: Bounds;
|
let bounds: Bounds;
|
||||||
|
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||||
@@ -110,7 +118,7 @@ export class ElementBounds {
|
|||||||
maxY + element.y,
|
maxY + element.y,
|
||||||
];
|
];
|
||||||
} else if (isLinearElement(element)) {
|
} else if (isLinearElement(element)) {
|
||||||
bounds = getLinearElementRotatedBounds(element, cx, cy);
|
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||||
} else if (element.type === "diamond") {
|
} else if (element.type === "diamond") {
|
||||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
||||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
||||||
@@ -153,15 +161,20 @@ export const getElementAbsoluteCoords = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
includeBoundText: boolean = false,
|
includeBoundText: boolean = false,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
|
const elementsMap =
|
||||||
|
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
return getFreeDrawElementAbsoluteCoords(element);
|
return getFreeDrawElementAbsoluteCoords(element);
|
||||||
} else if (isLinearElement(element)) {
|
} else if (isLinearElement(element)) {
|
||||||
return LinearElementEditor.getElementAbsoluteCoords(
|
return LinearElementEditor.getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
includeBoundText,
|
includeBoundText,
|
||||||
);
|
);
|
||||||
} else if (isTextElement(element)) {
|
} else if (isTextElement(element)) {
|
||||||
const container = getContainerElement(element);
|
const container = elementsMap
|
||||||
|
? getContainerElement(element, elementsMap)
|
||||||
|
: null;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
const coords = LinearElementEditor.getBoundTextElementPosition(
|
||||||
container,
|
container,
|
||||||
@@ -672,7 +685,10 @@ const getLinearElementRotatedBounds = (
|
|||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
cx: number,
|
cx: number,
|
||||||
cy: number,
|
cy: number,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = rotate(
|
const [x, y] = rotate(
|
||||||
@@ -684,7 +700,6 @@ const getLinearElementRotatedBounds = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
let coords: Bounds = [x, y, x, y];
|
let coords: Bounds = [x, y, x, y];
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
element,
|
element,
|
||||||
@@ -709,7 +724,6 @@ const getLinearElementRotatedBounds = (
|
|||||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
element,
|
element,
|
||||||
@@ -729,10 +743,8 @@ const getLinearElementRotatedBounds = (
|
|||||||
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||||
return ElementBounds.getBounds(element);
|
return ElementBounds.getBounds(element);
|
||||||
};
|
};
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||||
elements: readonly ExcalidrawElement[],
|
if ("size" in elements ? !elements.size : !elements.length) {
|
||||||
): Bounds => {
|
|
||||||
if (!elements.length) {
|
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
StrokeRoundness,
|
StrokeRoundness,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -78,6 +79,7 @@ export const hitTest = (
|
|||||||
frameNameBoundsCache: FrameNameBoundsCache,
|
frameNameBoundsCache: FrameNameBoundsCache,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// How many pixels off the shape boundary we still consider a hit
|
// How many pixels off the shape boundary we still consider a hit
|
||||||
const threshold = 10 / appState.zoom.value;
|
const threshold = 10 / appState.zoom.value;
|
||||||
@@ -95,7 +97,7 @@ export const hitTest = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const isHittingBoundTextElement = hitTest(
|
const isHittingBoundTextElement = hitTest(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
@@ -103,6 +105,7 @@ export const hitTest = (
|
|||||||
frameNameBoundsCache,
|
frameNameBoundsCache,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (isHittingBoundTextElement) {
|
if (isHittingBoundTextElement) {
|
||||||
return true;
|
return true;
|
||||||
@@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
|
|||||||
frameNameBoundsCache: FrameNameBoundsCache,
|
frameNameBoundsCache: FrameNameBoundsCache,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = 10 / appState.zoom.value;
|
const threshold = 10 / appState.zoom.value;
|
||||||
|
|
||||||
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
|
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
|
||||||
// eg for linear elements text can be outside the element bounding box
|
// eg for linear elements text can be outside the element bounding box
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (
|
if (
|
||||||
boundTextElement &&
|
boundTextElement &&
|
||||||
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
|
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ExcalidrawTextContainer } from "./types";
|
||||||
|
|
||||||
|
export const originalContainerCache: {
|
||||||
|
[id: ExcalidrawTextContainer["id"]]:
|
||||||
|
| {
|
||||||
|
height: ExcalidrawTextContainer["height"];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
export const updateOriginalContainerCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
height: ExcalidrawTextContainer["height"],
|
||||||
|
) => {
|
||||||
|
const data =
|
||||||
|
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||||
|
data.height = height;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetOriginalContainerCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
) => {
|
||||||
|
if (originalContainerCache[id]) {
|
||||||
|
delete originalContainerCache[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOriginalContainerHeightFromCache = (
|
||||||
|
id: ExcalidrawTextContainer["id"],
|
||||||
|
) => {
|
||||||
|
return originalContainerCache[id]?.height ?? null;
|
||||||
|
};
|
||||||
@@ -5,14 +5,9 @@ import { getPerfectElementSize } from "./sizeHelpers";
|
|||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
|
||||||
import { getGridPoint } from "../math";
|
import { getGridPoint } from "../math";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import { isArrowElement, isFrameLikeElement } from "./typeChecks";
|
||||||
isArrowElement,
|
|
||||||
isBoundToContainer,
|
|
||||||
isFrameLikeElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -37,13 +32,11 @@ export const dragSelectedElements = (
|
|||||||
.map((f) => f.id);
|
.map((f) => f.id);
|
||||||
|
|
||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
const elementsInFrames = scene
|
for (const element of scene.getNonDeletedElements()) {
|
||||||
.getNonDeletedElements()
|
if (element.frameId !== null && frames.includes(element.frameId)) {
|
||||||
.filter((e) => !isBoundToContainer(e))
|
elementsToUpdate.add(element);
|
||||||
.filter((e) => e.frameId !== null)
|
}
|
||||||
.filter((e) => frames.includes(e.frameId!));
|
}
|
||||||
|
|
||||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonBounds = getCommonBounds(
|
const commonBounds = getCommonBounds(
|
||||||
@@ -60,18 +53,14 @@ export const dragSelectedElements = (
|
|||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||||
// update coords of bound text only if we're dragging the container directly
|
|
||||||
// (we don't drag the group that it's part of)
|
|
||||||
if (
|
if (
|
||||||
// Don't update coords of arrow label since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
!isArrowElement(element) &&
|
!isArrowElement(element)
|
||||||
// container isn't part of any group
|
|
||||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
|
||||||
(!element.groupIds.length ||
|
|
||||||
// container is part of a group, but we're dragging the container directly
|
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
|
||||||
) {
|
) {
|
||||||
const textElement = getBoundTextElement(element);
|
const textElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
if (textElement) {
|
if (textElement) {
|
||||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { register } from "../actions/register";
|
import { register } from "../actions/register";
|
||||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||||
import { t } from "../i18n";
|
|
||||||
import { ExcalidrawProps } from "../types";
|
import { ExcalidrawProps } from "../types";
|
||||||
import { getFontString, updateActiveTool } from "../utils";
|
import { getFontString, updateActiveTool } from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { newTextElement } from "./newElement";
|
import { newTextElement } from "./newElement";
|
||||||
import { getContainerElement, wrapText } from "./textElement";
|
import { wrapText } from "./textElement";
|
||||||
import {
|
import { isIframeElement } from "./typeChecks";
|
||||||
isFrameLikeElement,
|
|
||||||
isIframeElement,
|
|
||||||
isIframeLikeElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
IframeData,
|
IframeData,
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const embeddedLinkCache = new Map<string, IframeData>();
|
const embeddedLinkCache = new Map<string, IframeData>();
|
||||||
@@ -32,9 +26,9 @@ const RE_GH_GIST_EMBED =
|
|||||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
||||||
|
|
||||||
// not anchored to start to allow <blockquote> twitter embeds
|
// not anchored to start to allow <blockquote> twitter embeds
|
||||||
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:twitter|x).com/;
|
||||||
const RE_TWITTER_EMBED =
|
const RE_TWITTER_EMBED =
|
||||||
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
/^<blockquote[\s\S]*?\shref=["'](https:\/\/(?:twitter|x).com\/[^"']*)/i;
|
||||||
|
|
||||||
const RE_VALTOWN =
|
const RE_VALTOWN =
|
||||||
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||||
@@ -54,6 +48,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"link.excalidraw.com",
|
"link.excalidraw.com",
|
||||||
"gist.github.com",
|
"gist.github.com",
|
||||||
"twitter.com",
|
"twitter.com",
|
||||||
|
"x.com",
|
||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
@@ -111,8 +106,8 @@ export const getEmbedLink = (
|
|||||||
const vimeoLink = link.match(RE_VIMEO);
|
const vimeoLink = link.match(RE_VIMEO);
|
||||||
if (vimeoLink?.[1]) {
|
if (vimeoLink?.[1]) {
|
||||||
const target = vimeoLink?.[1];
|
const target = vimeoLink?.[1];
|
||||||
const warning = !/^\d+$/.test(target)
|
const error = !/^\d+$/.test(target)
|
||||||
? t("toast.unrecognizedLinkFormat")
|
? new URIError("Invalid embed link format")
|
||||||
: undefined;
|
: undefined;
|
||||||
type = "video";
|
type = "video";
|
||||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||||
@@ -124,7 +119,7 @@ export const getEmbedLink = (
|
|||||||
intrinsicSize: aspectRatio,
|
intrinsicSize: aspectRatio,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
return { link, intrinsicSize: aspectRatio, type, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const figmaLink = link.match(RE_FIGMA);
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
@@ -155,6 +150,9 @@ export const getEmbedLink = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (RE_TWITTER.test(link)) {
|
if (RE_TWITTER.test(link)) {
|
||||||
|
// the embed srcdoc still supports twitter.com domain only
|
||||||
|
link = link.replace(/\bx.com\b/, "twitter.com");
|
||||||
|
|
||||||
let ret: IframeData;
|
let ret: IframeData;
|
||||||
// assume embed code
|
// assume embed code
|
||||||
if (/<blockquote/.test(link)) {
|
if (/<blockquote/.test(link)) {
|
||||||
@@ -213,21 +211,6 @@ export const getEmbedLink = (
|
|||||||
return { link, intrinsicSize: aspectRatio, type };
|
return { link, intrinsicSize: aspectRatio, type };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isIframeLikeOrItsLabel = (
|
|
||||||
element: NonDeletedExcalidrawElement,
|
|
||||||
): Boolean => {
|
|
||||||
if (isIframeLikeElement(element)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (element.type === "text") {
|
|
||||||
const container = getContainerElement(element);
|
|
||||||
if (container && isFrameLikeElement(container)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPlaceholderEmbeddableLabel = (
|
export const createPlaceholderEmbeddableLabel = (
|
||||||
element: ExcalidrawIframeLikeElement,
|
element: ExcalidrawIframeLikeElement,
|
||||||
): ExcalidrawElement => {
|
): ExcalidrawElement => {
|
||||||
@@ -321,26 +304,26 @@ const validateHostname = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractSrc = (htmlString: string): string => {
|
export const maybeParseEmbedSrc = (str: string): string => {
|
||||||
const twitterMatch = htmlString.match(RE_TWITTER_EMBED);
|
const twitterMatch = str.match(RE_TWITTER_EMBED);
|
||||||
if (twitterMatch && twitterMatch.length === 2) {
|
if (twitterMatch && twitterMatch.length === 2) {
|
||||||
return twitterMatch[1];
|
return twitterMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
|
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
||||||
if (gistMatch && gistMatch.length === 2) {
|
if (gistMatch && gistMatch.length === 2) {
|
||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RE_GIPHY.test(htmlString)) {
|
if (RE_GIPHY.test(str)) {
|
||||||
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
|
return `https://giphy.com/embed/${RE_GIPHY.exec(str)![1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
const match = str.match(RE_GENERIC_EMBED);
|
||||||
if (match && match.length === 2) {
|
if (match && match.length === 2) {
|
||||||
return match[1];
|
return match[1];
|
||||||
}
|
}
|
||||||
return htmlString;
|
return str;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const embeddableURLValidator = (
|
export const embeddableURLValidator = (
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export {
|
|||||||
dragNewElement,
|
dragNewElement,
|
||||||
} from "./dragElements";
|
} from "./dragElements";
|
||||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||||
export { textWysiwyg } from "./textWysiwyg";
|
|
||||||
export { redrawTextBoundingBox } from "./textElement";
|
export { redrawTextBoundingBox } from "./textElement";
|
||||||
export {
|
export {
|
||||||
getPerfectElementSize,
|
getPerfectElementSize,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
PointBinding,
|
PointBinding,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
distance2d,
|
distance2d,
|
||||||
@@ -193,6 +194,7 @@ export class LinearElementEditor {
|
|||||||
pointSceneCoords: { x: number; y: number }[],
|
pointSceneCoords: { x: number; y: number }[],
|
||||||
) => void,
|
) => void,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!linearElementEditor) {
|
if (!linearElementEditor) {
|
||||||
return false;
|
return false;
|
||||||
@@ -272,9 +274,9 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
handleBindTextResize(element, false);
|
handleBindTextResize(element, elementsMap, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
@@ -404,9 +406,10 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getEditorMidPoints = (
|
static getEditorMidPoints = (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): typeof editorMidPointsCache["points"] => {
|
): typeof editorMidPointsCache["points"] => {
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||||
if (
|
if (
|
||||||
@@ -465,6 +468,7 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const { elementId } = linearElementEditor;
|
const { elementId } = linearElementEditor;
|
||||||
const element = LinearElementEditor.getElement(elementId);
|
const element = LinearElementEditor.getElement(elementId);
|
||||||
@@ -503,7 +507,7 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const midPoints: typeof editorMidPointsCache["points"] =
|
const midPoints: typeof editorMidPointsCache["points"] =
|
||||||
LinearElementEditor.getEditorMidPoints(element, appState);
|
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (midPoints[index] !== null) {
|
if (midPoints[index] !== null) {
|
||||||
const distance = distance2d(
|
const distance = distance2d(
|
||||||
@@ -581,6 +585,7 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
midPoint: Point,
|
midPoint: Point,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) {
|
) {
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElementEditor.elementId,
|
linearElementEditor.elementId,
|
||||||
@@ -588,7 +593,11 @@ export class LinearElementEditor {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
|
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
|
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
|
||||||
@@ -605,6 +614,7 @@ export class LinearElementEditor {
|
|||||||
history: History,
|
history: History,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): {
|
): {
|
||||||
didAddPoint: boolean;
|
didAddPoint: boolean;
|
||||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||||
@@ -630,6 +640,7 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor,
|
linearElementEditor,
|
||||||
scenePointer,
|
scenePointer,
|
||||||
appState,
|
appState,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
let segmentMidpointIndex = null;
|
let segmentMidpointIndex = null;
|
||||||
if (segmentMidpoint) {
|
if (segmentMidpoint) {
|
||||||
@@ -637,6 +648,7 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor,
|
linearElementEditor,
|
||||||
appState,
|
appState,
|
||||||
segmentMidpoint,
|
segmentMidpoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (event.altKey && appState.editingLinearElement) {
|
if (event.altKey && appState.editingLinearElement) {
|
||||||
@@ -1418,6 +1430,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getElementAbsoluteCoords = (
|
static getElementAbsoluteCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
includeBoundText: boolean = false,
|
includeBoundText: boolean = false,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
let coords: [number, number, number, number, number, number];
|
let coords: [number, number, number, number, number, number];
|
||||||
@@ -1462,7 +1475,7 @@ export class LinearElementEditor {
|
|||||||
if (!includeBoundText) {
|
if (!includeBoundText) {
|
||||||
return coords;
|
return coords;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
coords = LinearElementEditor.getMinMaxXYWithBoundText(
|
coords = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
element,
|
element,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getContainerElement,
|
|
||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
@@ -136,13 +135,9 @@ export const newElement = (
|
|||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: "embeddable";
|
type: "embeddable";
|
||||||
validated: ExcalidrawEmbeddableElement["validated"];
|
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
||||||
return {
|
return _newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts);
|
||||||
..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts),
|
|
||||||
validated: opts.validated,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newIframeElement = (
|
export const newIframeElement = (
|
||||||
@@ -337,17 +332,17 @@ const getAdjustedDimensions = (
|
|||||||
|
|
||||||
export const refreshTextDimensions = (
|
export const refreshTextDimensions = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
text = textElement.text,
|
text = textElement.text,
|
||||||
) => {
|
) => {
|
||||||
if (textElement.isDeleted) {
|
if (textElement.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const container = getContainerElement(textElement);
|
|
||||||
if (container) {
|
if (container) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
getBoundTextMaxWidth(container),
|
getBoundTextMaxWidth(container, textElement),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
@@ -356,6 +351,7 @@ export const refreshTextDimensions = (
|
|||||||
|
|
||||||
export const updateTextElement = (
|
export const updateTextElement = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
{
|
{
|
||||||
text,
|
text,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
@@ -369,7 +365,7 @@ export const updateTextElement = (
|
|||||||
return newElementWith(textElement, {
|
return newElementWith(textElement, {
|
||||||
originalText,
|
originalText,
|
||||||
isDeleted: isDeleted ?? textElement.isDeleted,
|
isDeleted: isDeleted ?? textElement.isDeleted,
|
||||||
...refreshTextDimensions(textElement, originalText),
|
...refreshTextDimensions(textElement, container, originalText),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +42,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState, Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
@@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => {
|
|||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
resizeArrowDirection: "origin" | "end",
|
elementsMap: ElementsMap,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
@@ -79,7 +80,6 @@ export const transformElements = (
|
|||||||
pointerY: number,
|
pointerY: number,
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
appState: AppState,
|
|
||||||
) => {
|
) => {
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
@@ -89,7 +89,6 @@ export const transformElements = (
|
|||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
pointerDownState.originalElements,
|
|
||||||
);
|
);
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -101,6 +100,7 @@ export const transformElements = (
|
|||||||
) {
|
) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
@@ -109,9 +109,10 @@ export const transformElements = (
|
|||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
resizeSingleElement(
|
resizeSingleElement(
|
||||||
pointerDownState.originalElements,
|
originalElements,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
@@ -123,8 +124,9 @@ export const transformElements = (
|
|||||||
} else if (selectedElements.length > 1) {
|
} else if (selectedElements.length > 1) {
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
rotateMultipleElements(
|
rotateMultipleElements(
|
||||||
pointerDownState,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
@@ -139,8 +141,9 @@ export const transformElements = (
|
|||||||
transformHandleType === "se"
|
transformHandleType === "se"
|
||||||
) {
|
) {
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
pointerDownState,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
@@ -157,7 +160,6 @@ const rotateSingleElement = (
|
|||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
|
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
@@ -207,6 +209,7 @@ const rescalePointsInElement = (
|
|||||||
|
|
||||||
const measureFontSizeFromWidth = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
): { size: number; baseline: number } | null => {
|
): { size: number; baseline: number } | null => {
|
||||||
@@ -215,9 +218,9 @@ const measureFontSizeFromWidth = (
|
|||||||
|
|
||||||
const hasContainer = isBoundToContainer(element);
|
const hasContainer = isBoundToContainer(element);
|
||||||
if (hasContainer) {
|
if (hasContainer) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element, elementsMap);
|
||||||
if (container) {
|
if (container) {
|
||||||
width = getBoundTextMaxWidth(container);
|
width = getBoundTextMaxWidth(container, element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextFontSize = element.fontSize * (nextWidth / width);
|
const nextFontSize = element.fontSize * (nextWidth / width);
|
||||||
@@ -257,6 +260,7 @@ const getSidesForTransformHandle = (
|
|||||||
|
|
||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
@@ -303,7 +307,12 @@ const resizeSingleTextElement = (
|
|||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
const nextWidth = element.width * scale;
|
const nextWidth = element.width * scale;
|
||||||
const nextHeight = element.height * scale;
|
const nextHeight = element.height * scale;
|
||||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
const metrics = measureFontSizeFromWidth(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
);
|
||||||
if (metrics === null) {
|
if (metrics === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,6 +351,7 @@ export const resizeSingleElement = (
|
|||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleDirection: TransformHandleDirection,
|
transformHandleDirection: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
@@ -385,7 +395,7 @@ export const resizeSingleElement = (
|
|||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||||
@@ -448,7 +458,8 @@ export const resizeSingleElement = (
|
|||||||
|
|
||||||
const nextFont = measureFontSizeFromWidth(
|
const nextFont = measureFontSizeFromWidth(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
getBoundTextMaxWidth(updatedElement),
|
elementsMap,
|
||||||
|
getBoundTextMaxWidth(updatedElement, boundTextElement),
|
||||||
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
||||||
);
|
);
|
||||||
if (nextFont === null) {
|
if (nextFont === null) {
|
||||||
@@ -630,6 +641,7 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
transformHandleDirection,
|
transformHandleDirection,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
);
|
);
|
||||||
@@ -637,8 +649,9 @@ export const resizeSingleElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const resizeMultipleElements = (
|
export const resizeMultipleElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
@@ -658,7 +671,7 @@ export const resizeMultipleElements = (
|
|||||||
}[],
|
}[],
|
||||||
element,
|
element,
|
||||||
) => {
|
) => {
|
||||||
const origElement = pointerDownState.originalElements.get(element.id);
|
const origElement = originalElements.get(element.id);
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
acc.push({ orig: origElement, latest: element });
|
acc.push({ orig: origElement, latest: element });
|
||||||
}
|
}
|
||||||
@@ -679,7 +692,7 @@ export const resizeMultipleElements = (
|
|||||||
if (!textId) {
|
if (!textId) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
const text = pointerDownState.originalElements.get(textId) ?? null;
|
const text = originalElements.get(textId) ?? null;
|
||||||
if (!isBoundToContainer(text)) {
|
if (!isBoundToContainer(text)) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
@@ -825,7 +838,12 @@ export const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(orig)) {
|
if (isTextElement(orig)) {
|
||||||
const metrics = measureFontSizeFromWidth(orig, width, height);
|
const metrics = measureFontSizeFromWidth(
|
||||||
|
orig,
|
||||||
|
elementsMap,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
if (!metrics) {
|
if (!metrics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -833,7 +851,7 @@ export const resizeMultipleElements = (
|
|||||||
update.baseline = metrics.baseline;
|
update.baseline = metrics.baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = pointerDownState.originalElements.get(
|
const boundTextElement = originalElements.get(
|
||||||
getBoundTextElementId(orig) ?? "",
|
getBoundTextElementId(orig) ?? "",
|
||||||
) as ExcalidrawTextElementWithContainer | undefined;
|
) as ExcalidrawTextElementWithContainer | undefined;
|
||||||
|
|
||||||
@@ -866,7 +884,7 @@ export const resizeMultipleElements = (
|
|||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && boundTextFontSize) {
|
if (boundTextElement && boundTextFontSize) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
@@ -876,7 +894,7 @@ export const resizeMultipleElements = (
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
handleBindTextResize(element, transformHandleType, true);
|
handleBindTextResize(element, elementsMap, transformHandleType, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,8 +902,9 @@ export const resizeMultipleElements = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
pointerDownState: PointerDownState,
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
@@ -906,8 +925,7 @@ const rotateMultipleElements = (
|
|||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const origAngle =
|
const origAngle =
|
||||||
pointerDownState.originalElements.get(element.id)?.angle ??
|
originalElements.get(element.id)?.angle ?? element.angle;
|
||||||
element.angle;
|
|
||||||
const [rotatedCX, rotatedCY] = rotate(
|
const [rotatedCX, rotatedCY] = rotate(
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
@@ -926,7 +944,7 @@ const rotateMultipleElements = (
|
|||||||
);
|
);
|
||||||
updateBoundElements(element, { simultaneouslyUpdated: elements });
|
updateBoundElements(element, { simultaneouslyUpdated: elements });
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !isArrowElement(element)) {
|
if (boundText && !isArrowElement(element)) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
boundText,
|
boundText,
|
||||||
|
|||||||
@@ -319,17 +319,17 @@ describe("Test measureText", () => {
|
|||||||
|
|
||||||
it("should return max width when container is rectangle", () => {
|
it("should return max width when container is rectangle", () => {
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
expect(getBoundTextMaxWidth(container)).toBe(168);
|
expect(getBoundTextMaxWidth(container, null)).toBe(168);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is ellipse", () => {
|
it("should return max width when container is ellipse", () => {
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
expect(getBoundTextMaxWidth(container)).toBe(116);
|
expect(getBoundTextMaxWidth(container, null)).toBe(116);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is diamond", () => {
|
it("should return max width when container is diamond", () => {
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
expect(getBoundTextMaxWidth(container)).toBe(79);
|
expect(getBoundTextMaxWidth(container, null)).toBe(79);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||||
import {
|
import {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
@@ -31,23 +31,22 @@ import { isTextBindableContainer } from "./typeChecks";
|
|||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||||
|
|
||||||
|
import { ExtractSetType } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./containerCache";
|
||||||
import { ExtractSetType } from "../utility-types";
|
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
text
|
normalizeEOL(text)
|
||||||
// replace tabs with spaces so they render and measure correctly
|
// replace tabs with spaces so they render and measure correctly
|
||||||
.replace(/\t/g, " ")
|
.replace(/\t/g, " ")
|
||||||
// normalize newlines
|
|
||||||
.replace(/\r?\n|\r/g, "\n")
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const splitIntoLines = (text: string) => {
|
const splitIntoLines = (text: string) => {
|
||||||
return normalizeText(text).split("\n");
|
return normalizeText(text).split("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ export const redrawTextBoundingBox = (
|
|||||||
container,
|
container,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
);
|
);
|
||||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
|
||||||
|
|
||||||
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||||
const nextHeight = computeContainerDimensionForBoundText(
|
const nextHeight = computeContainerDimensionForBoundText(
|
||||||
@@ -163,6 +162,7 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
shouldMaintainAspectRatio = false,
|
||||||
) => {
|
) => {
|
||||||
@@ -171,25 +171,17 @@ export const handleBindTextResize = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetOriginalContainerCache(container.id);
|
resetOriginalContainerCache(container.id);
|
||||||
let textElement = Scene.getScene(container)!.getElement(
|
const textElement = getBoundTextElement(container, elementsMap);
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElement;
|
|
||||||
if (textElement && textElement.text) {
|
if (textElement && textElement.text) {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
textElement = Scene.getScene(container)!.getElement(
|
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElement;
|
|
||||||
let text = textElement.text;
|
let text = textElement.text;
|
||||||
let nextHeight = textElement.height;
|
let nextHeight = textElement.height;
|
||||||
let nextWidth = textElement.width;
|
let nextWidth = textElement.width;
|
||||||
const maxWidth = getBoundTextMaxWidth(container);
|
const maxWidth = getBoundTextMaxWidth(container, textElement);
|
||||||
const maxHeight = getBoundTextMaxHeight(
|
const maxHeight = getBoundTextMaxHeight(container, textElement);
|
||||||
container,
|
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
|
||||||
);
|
|
||||||
let containerHeight = container.height;
|
let containerHeight = container.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (
|
if (
|
||||||
@@ -244,10 +236,7 @@ export const handleBindTextResize = (
|
|||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
textElement,
|
textElement,
|
||||||
computeBoundTextPosition(
|
computeBoundTextPosition(container, textElement),
|
||||||
container,
|
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,7 +254,7 @@ export const computeBoundTextPosition = (
|
|||||||
}
|
}
|
||||||
const containerCoords = getContainerCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
||||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
|
||||||
|
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
@@ -668,33 +657,32 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
|||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
export const getBoundTextElement = (
|
||||||
|
element: ExcalidrawElement | null,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
return (
|
return (elementsMap.get(boundTextElementId) ||
|
||||||
(Scene.getScene(element)?.getElement(
|
null) as ExcalidrawTextElementWithContainer | null;
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElementWithContainer) || null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainerElement = (
|
export const getContainerElement = (
|
||||||
element:
|
element: ExcalidrawTextElement | null,
|
||||||
| (ExcalidrawElement & {
|
elementsMap: ElementsMap,
|
||||||
containerId: ExcalidrawElement["id"] | null;
|
): ExcalidrawTextContainer | null => {
|
||||||
})
|
|
||||||
| null,
|
|
||||||
) => {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (element.containerId) {
|
if (element.containerId) {
|
||||||
return Scene.getScene(element)?.getElement(element.containerId) || null;
|
return (elementsMap.get(element.containerId) ||
|
||||||
|
null) as ExcalidrawTextContainer | null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -702,6 +690,7 @@ export const getContainerElement = (
|
|||||||
export const getContainerCenter = (
|
export const getContainerCenter = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
return {
|
return {
|
||||||
@@ -721,6 +710,7 @@ export const getContainerCenter = (
|
|||||||
const index = container.points.length / 2 - 1;
|
const index = container.points.length / 2 - 1;
|
||||||
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
||||||
container,
|
container,
|
||||||
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
)[index];
|
)[index];
|
||||||
if (!midSegmentMidpoint) {
|
if (!midSegmentMidpoint) {
|
||||||
@@ -754,28 +744,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
export const getTextElementAngle = (
|
||||||
const container = getContainerElement(textElement);
|
textElement: ExcalidrawTextElement,
|
||||||
|
container: ExcalidrawTextContainer | null,
|
||||||
|
) => {
|
||||||
if (!container || isArrowElement(container)) {
|
if (!container || isArrowElement(container)) {
|
||||||
return textElement.angle;
|
return textElement.angle;
|
||||||
}
|
}
|
||||||
return container.angle;
|
return container.angle;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElementOffset = (
|
|
||||||
boundTextElement: ExcalidrawTextElement | null,
|
|
||||||
) => {
|
|
||||||
const container = getContainerElement(boundTextElement);
|
|
||||||
if (!container || !boundTextElement) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
return BOUND_TEXT_PADDING * 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BOUND_TEXT_PADDING;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundTextElementPosition = (
|
export const getBoundTextElementPosition = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
@@ -790,12 +768,12 @@ export const getBoundTextElementPosition = (
|
|||||||
|
|
||||||
export const shouldAllowVerticalAlign = (
|
export const shouldAllowVerticalAlign = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return selectedElements.some((element) => {
|
return selectedElements.some((element) => {
|
||||||
const hasBoundContainer = isBoundToContainer(element);
|
if (isBoundToContainer(element)) {
|
||||||
if (hasBoundContainer) {
|
const container = getContainerElement(element, elementsMap);
|
||||||
const container = getContainerElement(element);
|
if (isArrowElement(container)) {
|
||||||
if (isTextElement(element) && isArrowElement(container)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -806,12 +784,12 @@ export const shouldAllowVerticalAlign = (
|
|||||||
|
|
||||||
export const suppportsHorizontalAlign = (
|
export const suppportsHorizontalAlign = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return selectedElements.some((element) => {
|
return selectedElements.some((element) => {
|
||||||
const hasBoundContainer = isBoundToContainer(element);
|
if (isBoundToContainer(element)) {
|
||||||
if (hasBoundContainer) {
|
const container = getContainerElement(element, elementsMap);
|
||||||
const container = getContainerElement(element);
|
if (isArrowElement(container)) {
|
||||||
if (isTextElement(element) && isArrowElement(container)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -892,9 +870,7 @@ export const computeContainerDimensionForBoundText = (
|
|||||||
|
|
||||||
export const getBoundTextMaxWidth = (
|
export const getBoundTextMaxWidth = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
|
boundTextElement: ExcalidrawTextElement | null,
|
||||||
container,
|
|
||||||
),
|
|
||||||
) => {
|
) => {
|
||||||
const { width } = container;
|
const { width } = container;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
|
|||||||