Compare commits

..

9 Commits

47 changed files with 411 additions and 1519 deletions

View File

@ -80,7 +80,7 @@ A given tab trigger button that switches to a given sidebar tab. It must be rend
| `className` | `string` | No | |
| `style` | `React.CSSProperties` | No | |
You can use the [`ref.toggleSidebar({ name: "custom" })`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api#toggleSidebar) api to control the sidebar, but we export a trigger button to make UI use cases easier.
You can use the [`ref.toggleSidebar({ name: "custom" })`](/docs/@excalidraw/excalidraw/api/props/ref#toggleSidebar) api to control the sidebar, but we export a trigger button to make UI use cases easier.
## Example

View File

@ -10,17 +10,13 @@ The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/
**_Signature_**
```ts
convertToExcalidrawElements(
elements: ExcalidrawElementSkeleton,
opts?: { regenerateIds: boolean }
): ExcalidrawElement[]
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L137) | | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. |
| `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. |
<pre>
convertToExcalidrawElements(elements:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133">
ExcalidrawElementSkeleton
</a>
)
</pre>
**_How to use_**
@ -28,13 +24,13 @@ convertToExcalidrawElements(
import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
```
This function converts the Excalidraw Element Skeleton to excalidraw elements which could be then rendered on the canvas. Hence calling this function is necessary before passing it to APIs like [`initialData`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/initialdata), [`updateScene`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#updatescene) if you are using the Skeleton API
This function converts the Excalidraw Element Skeleton to excalidraw elements which could be then rendered on the canvas. Hence calling this function is necessary before passing it to APIs like [`initialData`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/initialdata), [`updateScene`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#updatescene) if you are using the Skeleton API
## Supported Features
### Rectangle, Ellipse, and Diamond
To create these shapes you need to pass its `type` and `x` and `y` coordinates for position. The rest of the attributes are optional\_.
To create these shapes you need to pass its `type` and `x` and `y` coordinates for position. The rest of the attributes are optional_.
For the Skeleton API to work, `convertToExcalidrawElements` needs to be called before passing it to Excalidraw Component via initialData, updateScene or any such API.
@ -431,45 +427,3 @@ convertToExcalidrawElements([
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/a8b047c8-2eed-4aea-82a2-e1e6bbddb8d4)
### Frames
To create a frame, you need to pass `type`, `children` (list of Excalidraw element ids). The rest of the attributes are optional.
```ts
{
type: "frame";
children: readonly ExcalidrawElement["id"][];
name?: string;
} & Partial<ExcalidrawFrameElement>);
```
```ts
convertToExcalidrawElements([
{
"type": "rectangle",
"x": 10,
"y": 10,
"strokeWidth": 2,
"id": "1"
},
{
"type": "diamond",
"x": 120,
"y": 20,
"backgroundColor": "#fff3bf",
"strokeWidth": 2,
"label": {
"text": "HELLO EXCALIDRAW",
"strokeColor": "#099268",
"fontSize": 30
},
"id": "2"
},
{
"type": "frame",
"children": ["1", "2"],
"name": "My frame"
}]
}
```

View File

@ -1,11 +1,11 @@
# Props
All `props` are _optional_.
All `props` are *optional*.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | _ | Callback triggered with the excalidraw api once rendered |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`ref`](/docs/@excalidraw/excalidraw/api/props/ref) | `object` | _ | `Ref` to be passed to Excalidraw |
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
@ -37,7 +37,7 @@ Beyond attributes that Excalidraw elements already support, you can store `custo
You can use this to add any extra information you need to keep track of.
You can add `customData` to elements when passing them as [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata), or using [`updateScene`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api#updatescene) / [`updateLibrary`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api#updatelibrary) afterwards.
You can add `customData` to elements when passing them as [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata), or using [`updateScene`](/docs/@excalidraw/excalidraw/api/props/ref#updatescene) / [`updateLibrary`](/docs/@excalidraw/excalidraw/api/props/ref#updatelibrary) afterwards.
```js showLineNumbers
{
@ -93,16 +93,8 @@ This callback is triggered when mouse pointer is updated.
This prop if passed will be triggered on pointer down events and has the below signature.
<pre>
(activeTool:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115">
{" "}
AppState["activeTool"]
</a>
, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">
PointerDownState
</a>) => void
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">PointerDownState</a>) => void
</pre>
### onScrollChange
@ -118,11 +110,7 @@ This prop if passed will be triggered when canvas is scrolled and has the below
This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs.
<pre>
(data:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">
ClipboardData
</a>
, event: ClipboardEvent &#124; null) => boolean
(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean
</pre>
This callback must return a `boolean` value or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to a boolean value.
@ -148,11 +136,8 @@ It is invoked with empty items when user clears the library. You can use this ca
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
<pre>
(element:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
, event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>,
event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
</pre>
Example:
@ -195,30 +180,30 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
### viewModeEnabled
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over _intialData.appState.viewModeEnabled_, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over *intialData.appState.viewModeEnabled*, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### zenModeEnabled
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over _intialData.appState.zenModeEnabled_, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over *intialData.appState.zenModeEnabled*, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### gridModeEnabled
This prop indicates whether the shows the grid. When supplied, the value takes precedence over _intialData.appState.gridModeEnabled_, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the shows the grid. When supplied, the value takes precedence over *intialData.appState.gridModeEnabled*, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### libraryReturnUrl
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com).
Defaults to _window.location.origin + window.location.pathname_. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
Defaults to *window.location.origin + window.location.pathname*. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
### theme
This prop controls Excalidraw's theme. When supplied, the value takes precedence over _intialData.appState.theme_, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless _UIOptions.canvasActions.toggleTheme_ is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
This prop controls Excalidraw's theme. When supplied, the value takes precedence over *intialData.appState.theme*, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless *UIOptions.canvasActions.toggleTheme* is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify the theme.
### name
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over _intialData.appState.name_, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over *intialData.appState.name*, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
### detectScroll
@ -251,4 +236,4 @@ validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) =>
This is an optional property. By default we support a handful of well-known sites. You may allow additional sites or disallow the default ones by supplying a custom validator. If you pass `true`, all URLs will be allowed. You can also supply a list of hostnames, RegExp (or list of RegExp objects), or a function. If the function returns `undefined`, the built-in validator will be used.
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.

View File

@ -1,26 +1,27 @@
# excalidrawAPI
# ref
<pre>
(api:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616">
ExcalidrawAPI
</a>
) => void;
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">
createRef
</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">
callbackRef
</a>{" "}
&#124; <br />
&#123; current: &#123; readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
resolvablePromise
</a> } }
</pre>
Once the callback is triggered, you will need to store the api in state to access it later.
```jsx showLineNumbers
export default function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
}
```
You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616). We expose the below APIs :point_down:
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
| API | Signature | Usage |
| --- | --- | --- |
| ready | `boolean` | This is set to true once Excalidraw is rendered |
| [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) |
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
| [addFiles](#addfiles) | `function` | add files data to the appState |
@ -38,15 +39,54 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
| [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 |
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
| [onChange](#onChange) | `function` | Subscribes to change events |
| [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
| [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
:::info The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the `excalidrawAPI`.
## readyPromise
Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0.
<pre>
const excalidrawRef = &#123; current:&#123; readyPromise:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
&nbsp;resolvablePromise
</a>
&nbsp;&#125; &#125;
</pre>
:::
Since plain object is passed as a `ref`, the `readyPromise` is resolved as soon as the component is mounted. Most of the time you will not need this unless you have a specific use case where you can't pass the `ref` in the react way and want to do some action on the host when this promise resolves.
```jsx showLineNumbers
const resolvablePromise = () => {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
};
const App = () => {
const excalidrawRef = useMemo(
() => ({
current: {
readyPromise: resolvablePromise(),
},
}),
[],
);
useEffect(() => {
excalidrawRef.current.readyPromise.then((api) => {
console.log("loaded", api);
});
}, [excalidrawRef]);
return (
<div style={{ height: "500px" }}>
<Excalidraw ref={excalidrawRef} />
</div>
);
};
```
## updateScene
@ -347,25 +387,14 @@ This API can be used to get the files present in the scene. It may contain files
This API has the below signature. It sets the `tool` passed in param as the active tool.
```ts
(
tool: (
| (
| { type: Exclude<ToolType, "image"> }
| {
type: Extract<ToolType, "image">;
insertOnCanvasDirectly?: boolean;
}
)
| { type: "custom"; customType: string }
) & { locked?: boolean },
) => {};
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
<pre>
(tool: <br /> &#123; type:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">
SHAPES
</a>
[number]["value"]&#124; "eraser" &#125; &#124;
<br /> &#123; type: "custom"; customType: string &#125;) => void
</pre>
## setCursor
@ -392,51 +421,3 @@ This API is especially useful when you render a custom [`<Sidebar/>`](/docs/@exc
```
This API can be used to reset to default mouse cursor.
## onChange
```tsx
(
callback: (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => void
) => () => void
```
Subscribes to change events, similar to [`props.onChange`](/docs/@excalidraw/excalidraw/api/props#onchange).
Returns an unsubscribe function.
## onPointerDown
```tsx
(
callback: (
activeTool: AppState["activeTool"],
pointerDownState: PointerDownState,
event: React.PointerEvent<HTMLElement>,
) => void,
) => () => void
```
Subscribes to canvas `pointerdown` events.
Returns an unsubscribe function.
## onPointerUp
```tsx
(
callback: (
activeTool: AppState["activeTool"],
pointerDownState: PointerDownState,
event: PointerEvent,
) => void,
) => () => void
```
Subscribes to canvas `pointerup` events.
Returns an unsubscribe function.

View File

@ -1,6 +1,6 @@
# UIOptions
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasactions), [`dockedSidebarBreakpoint`](#dockedsidebarbreakpoint) [`welcomeScreen`](#welcmescreen) and [`tools`](#tools).
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasactions), [`dockedSidebarBreakpoint`](#dockedsidebarbreakpoint) and [`welcomeScreen`](#welcmescreen).
<pre>
&#123;
@ -70,12 +70,3 @@ function App() {
);
}
```
## tools
This `prop ` controls the visibility of the tools in the editor.
Currently you can control the visibility of `image` tool via this prop.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| image | boolean | true | Decides whether `image` tool should be visible.

View File

@ -129,7 +129,7 @@ if (contents.type === MIME_TYPES.excalidraw) {
<pre>
loadSceneOrLibraryFromBlob(<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
fileHandle?: FileSystemHandle | null<br/>
@ -164,9 +164,9 @@ import { isLinearElement } from "@excalidraw/excalidraw";
**Signature**
<pre>
```tsx
isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean
</pre>
```
### getNonDeletedElements
@ -195,10 +195,8 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw";
**_Signature_**
<pre>
mergeLibraryItems(<br/>&nbsp;
localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a><br/>
): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
</pre>
### parseLibraryTokensFromUrl
@ -333,15 +331,13 @@ const App = () => (
render(<App />);
```
The `device` has the following `attributes`, some grouped into `viewport` and `editor` objects, per context.
The `device` has the following `attributes`
| Name | Type | Description |
| --- | --- | --- |
| `viewport.isMobile` | `boolean` | Set to `true` when viewport is in `mobile` breakpoint |
| `viewport.isLandscape` | `boolean` | Set to `true` when the viewport is in `landscape` mode |
| `editor.canFitSidebar` | `boolean` | Set to `true` if there's enough space to fit the `sidebar` |
| `editor.isMobile` | `boolean` | Set to `true` when editor container is in `mobile` breakpoint |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` when touch event detected |
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
### i18n
@ -386,94 +382,3 @@ function App() {
);
}
```
### getCommonBounds
This util can be used to get the common bounds of the passed elements.
**_Signature_**
```ts
getCommonBounds(
elements: readonly ExcalidrawElement[]
): readonly [
minX: number,
minY: number,
maxX: number,
maxY: number,
]
```
**_How to use_**
```js
import { getCommonBounds } from "@excalidraw/excalidraw";
```
### elementsOverlappingBBox
To filter `elements` that are inside, overlap, or contain the `bounds` rectangle.
The bounds check is approximate and does not precisely follow the element's shape. You can also supply `errorMargin` which effectively makes the `bounds` larger by that amount.
This API has 3 `type`s of operation: `overlap`, `contain`, and `inside`:
- `overlap` - filters elements that are overlapping or inside bounds.
- `contain` - filters elements that are inside bounds or bounds inside elements.
- `inside` - filters elements that are inside bounds.
**_Signature_**
<pre>
elementsOverlappingBBox(<br/>&nbsp;
elements: readonly NonDeletedExcalidrawElement[];<br/>&nbsp;
bounds: <a href="https://github.com/excalidraw/excalidraw/blob/9c425224c789d083bf16e0597ce4a429b9ee008e/src/element/bounds.ts#L37-L42">Bounds</a> | ExcalidrawElement;<br/>&nbsp;
errorMargin?: number;<br/>&nbsp;
type: "overlap" | "contain" | "inside";<br/>
): NonDeletedExcalidrawElement[];
</pre>
**_How to use_**
```js
import { elementsOverlappingBBox } from "@excalidraw/excalidraw";
```
### isElementInsideBBox
Lower-level API than `elementsOverlappingBBox` to check if a single `element` is inside `bounds`. If `eitherDirection=true`, returns `true` if `element` is fully inside `bounds` rectangle, or vice versa. When `false`, it returns `true` only for the former case.
**_Signature_**
<pre>
isElementInsideBBox(<br/>&nbsp;
element: NonDeletedExcalidrawElement,<br/>&nbsp;
bounds: <a href="https://github.com/excalidraw/excalidraw/blob/9c425224c789d083bf16e0597ce4a429b9ee008e/src/element/bounds.ts#L37-L42">Bounds</a>,<br/>&nbsp;
eitherDirection = false,<br/>
): boolean
</pre>
**_How to use_**
```js
import { isElementInsideBBox } from "@excalidraw/excalidraw";
```
### elementPartiallyOverlapsWithOrContainsBBox
Checks if `element` is overlapping the `bounds` rectangle, or is fully inside.
**_Signature_**
<pre>
elementPartiallyOverlapsWithOrContainsBBox(<br/>&nbsp;
element: NonDeletedExcalidrawElement,<br/>&nbsp;
bounds: <a href="https://github.com/excalidraw/excalidraw/blob/9c425224c789d083bf16e0597ce4a429b9ee008e/src/element/bounds.ts#L37-L42">Bounds</a>,<br/>
): boolean
</pre>
**_How to use_**
```js
import { elementPartiallyOverlapsWithOrContainsBBox } from "@excalidraw/excalidraw";
```

View File

@ -43,7 +43,7 @@ Once the version is released `@excalibot` will post a comment with the release v
To release the next stable version follow the below steps:
```bash
yarn prerelease:excalidraw
yarn prerelease version
```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
@ -51,7 +51,7 @@ You need to pass the `version` for which you want to create the release. This wi
The next step is to run the `release` script:
```bash
yarn release:excalidraw
yarn release
```
This will publish the package.

View File

@ -31,17 +31,6 @@ We strongly recommend turning it off. You can follow the steps below on how to d
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
### ReferenceError: process is not defined
When using `vite` or any build tools, you will have to make sure the `process` is accessible as we are accessing `process.env.IS_PREACT` to decide whether to use `preact` build.
Since Vite removes env variables by default, you can update the vite config to ensure its available :point_down:
```
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
},
```
## Need help?

View File

@ -30,35 +30,13 @@ function App() {
}
```
### Next.js
### Rendering Excalidraw only on client
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
Here are two ways on how you can render **Excalidraw** on **Next.js**.
1. Using **Next.js Dynamic** import [Recommended].
Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`.
```jsx showLineNumbers
import dynamic from "next/dynamic";
const Excalidraw = dynamic(
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
{
ssr: false,
},
);
export default function App() {
return <Excalidraw />;
}
```
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
2. Importing Excalidraw once **client** is rendered.
1. Importing Excalidraw once **client** is rendered.
```jsx showLineNumbers
import { useState, useEffect } from "react";
@ -75,28 +53,26 @@ export default function 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)
2. Using **Next.js Dynamic** import.
### Preact
Since Excalidraw doesn't server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. However one drawback is the `Refs` don't work with dynamic import in Next.js. We are working on overcoming this and have a better API.
Since we support `umd` build ships with `react/jsx-runtime` and `react-dom/client` inlined with the package. This conflicts with `Preact` and hence the build doesn't work directly with `Preact`.
However we have shipped a separate build for `Preact` so if you are using `Preact` you need to set `process.env.IS_PREACT` to `true` to use the `Preact` build.
Once the above `env` variable is set, you will be able to use the package in `Preact` as well.
:::info
When using `vite` or any build tools, you will have to make sure the `process` is accessible as we are accessing `process.env.IS_PREACT` to decide whether to use `preact` build.
Since Vite removes env variables by default, you can update the vite config to ensure its available :point_down:
```
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
```jsx showLineNumbers
import dynamic from "next/dynamic";
const Excalidraw = dynamic(
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
{
ssr: false,
},
);
export default function App() {
return <Excalidraw />;
}
```
:::
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
## Browser

View File

@ -1,11 +1,6 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
// Set the env variable to false so the excalidraw npm package doesn't throw
// process undefined as docusaurus doesn't expose env variables by default
process.env.IS_PREACT = "false";
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Excalidraw developer docs",
@ -144,15 +139,7 @@ const config = {
},
}),
themes: ["@docusaurus/theme-live-codeblock"],
plugins: [
"docusaurus-plugin-sass",
[
"docusaurus2-dotenv",
{
systemvars: true,
},
],
],
plugins: ["docusaurus-plugin-sass"],
};
module.exports = config;

View File

@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.17.0",
"@excalidraw/excalidraw": "0.15.2-eb020d0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",
@ -30,7 +30,6 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
"@tsconfig/docusaurus": "^1.0.5",
"docusaurus2-dotenv": "1.4.0",
"typescript": "^4.7.4"
},
"browserslist": {

View File

@ -53,7 +53,7 @@ const sidebars = {
},
items: [
"@excalidraw/excalidraw/api/props/initialdata",
"@excalidraw/excalidraw/api/props/excalidraw-api",
"@excalidraw/excalidraw/api/props/ref",
"@excalidraw/excalidraw/api/props/render-props",
"@excalidraw/excalidraw/api/props/ui-options",
],

View File

@ -1718,10 +1718,10 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.0.tgz#3c64aa8e36406ac171b008cfecbdce5bb0755725"
integrity sha512-NzP22v5xMqxYW27ZtTHhiGFe7kE8NeBk45aoeM/mDSkXiOXPDH+PcvwzHRN/Ei+Vj/0sTPHxejn8bZyRWKGjXg==
"@excalidraw/excalidraw@0.15.2-eb020d0":
version "0.15.2-eb020d0"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-eb020d0.tgz#25bd61e6f23da7c084fb16a3e0fe0dd9ad8e6533"
integrity sha512-TKGLzpOVqFQcwK1GFKTDXgg1s2U6tc5KE3qXuv87osbzOtftQn3x4+VH61vwdj11l00nEN80SMdXUC43T9uJqQ==
"@hapi/hoek@^9.0.0":
version "9.3.0"
@ -3567,13 +3567,6 @@ docusaurus-plugin-sass@0.2.3:
dependencies:
sass-loader "^10.1.1"
docusaurus2-dotenv@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/docusaurus2-dotenv/-/docusaurus2-dotenv-1.4.0.tgz#9ab900e29de9081f9f1f28f7224ff63760385641"
integrity sha512-iWqem5fnBAyeBBtX75Fxp71uUAnwFaXzOmade8zAhN4vL3RG9m27sLSRwjJGVVgIkEo3esjGyCcTGTiCjfi+sg==
dependencies:
dotenv-webpack "1.7.0"
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -3659,25 +3652,6 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
dotenv-defaults@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-1.1.1.tgz#032c024f4b5906d9990eb06d722dc74cc60ec1bd"
integrity sha512-6fPRo9o/3MxKvmRZBD3oNFdxODdhJtIy1zcJeUSCs6HCy4tarUpd+G67UTU9tF6OWXeSPqsm4fPAB+2eY9Rt9Q==
dependencies:
dotenv "^6.2.0"
dotenv-webpack@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz#4384d8c57ee6f405c296278c14a9f9167856d3a1"
integrity sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==
dependencies:
dotenv-defaults "^1.0.2"
dotenv@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
duplexer3@^0.1.4:
version "0.1.5"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e"

View File

@ -128,8 +128,8 @@
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui --coverage.enabled=true",
"autorelease": "node scripts/autorelease.js",
"prerelease:excalidraw": "node scripts/prerelease.js",
"prerelease": "node scripts/prerelease.js",
"build:preview": "yarn build && vite preview --port 5000",
"release:excalidraw": "node scripts/release.js"
"release": "node scripts/release.js"
}
}

View File

@ -13,7 +13,7 @@ import {
hasStrokeWidth,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import { AppClassProperties, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
@ -218,12 +218,10 @@ export const ShapesSwitcher = ({
activeTool,
appState,
app,
UIOptions,
}: {
activeTool: UIAppState["activeTool"];
appState: UIAppState;
app: AppClassProperties;
UIOptions: AppProps["UIOptions"];
}) => {
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
@ -234,14 +232,6 @@ export const ShapesSwitcher = ({
return (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
if (
UIOptions.tools?.[
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
] === false
) {
return null;
}
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);

View File

@ -341,7 +341,6 @@ import {
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { ImageSceneDataError } from "../errors";
import {
getSnapLinesAtPointer,
snapDraggedElements,
@ -946,11 +945,7 @@ class App extends React.Component<AppProps, AppState> {
title="Excalidraw Embedded Content"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
sandbox={`${
embedLink?.sandbox?.allowSameOrigin
? "allow-same-origin"
: ""
} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads`}
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads"
/>
)}
</div>
@ -2277,11 +2272,6 @@ class App extends React.Component<AppProps, AppState> {
// prefer spreadsheet data over image file (MS Office/Libre Office)
if (isSupportedImageFile(file) && !data.spreadsheet) {
if (!this.isToolSupported("image")) {
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
return;
}
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
@ -2487,8 +2477,7 @@ class App extends React.Component<AppProps, AppState> {
) {
if (
!isPlainPaste &&
mixedContent.some((node) => node.type === "imageUrl") &&
this.isToolSupported("image")
mixedContent.some((node) => node.type === "imageUrl")
) {
const imageURLs = mixedContent
.filter((node) => node.type === "imageUrl")
@ -3295,16 +3284,6 @@ class App extends React.Component<AppProps, AppState> {
}
});
// We purposely widen the `tool` type so this helper can be called with
// any tool without having to type check it
private isToolSupported = <T extends ToolType | "custom">(tool: T) => {
return (
this.props.UIOptions.tools?.[
tool as Extract<T, keyof AppProps["UIOptions"]["tools"]>
] !== false
);
};
setActiveTool = (
tool: (
| (
@ -3317,13 +3296,6 @@ class App extends React.Component<AppProps, AppState> {
| { type: "custom"; customType: string }
) & { locked?: boolean },
) => {
if (!this.isToolSupported(tool.type)) {
console.warn(
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`,
);
return;
}
const nextActiveTool = updateActiveTool(this.state, tool);
if (nextActiveTool.type === "hand") {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
@ -4768,13 +4740,9 @@ class App extends React.Component<AppProps, AppState> {
});
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
mutateElement(pendingImageElement, {
x,
y,
frameId: frame ? frame.id : null,
});
} else if (this.state.activeTool.type === "freedraw") {
this.handleFreeDrawElementOnPointerDown(
@ -5641,11 +5609,9 @@ class App extends React.Component<AppProps, AppState> {
private createImageElement = ({
sceneX,
sceneY,
addToFrameUnderCursor = true,
}: {
sceneX: number;
sceneY: number;
addToFrameUnderCursor?: boolean;
}) => {
const [gridX, gridY] = getGridPoint(
sceneX,
@ -5655,12 +5621,10 @@ class App extends React.Component<AppProps, AppState> {
: this.state.gridSize,
);
const topLayerFrame = addToFrameUnderCursor
? this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY,
})
: null;
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY,
});
const element = newImageElement({
type: "image",
@ -6904,10 +6868,13 @@ class App extends React.Component<AppProps, AppState> {
topLayerFrame &&
!this.state.selectedElementIds[topLayerFrame.id]
) {
const processedGroupIds = new Map<string, boolean>();
const elementsToAdd = selectedElements.filter(
(element) =>
element.frameId !== topLayerFrame.id &&
isElementInFrame(element, nextElements, this.state),
isElementInFrame(element, nextElements, this.state, {
processedGroupIds,
}),
);
if (this.state.editingGroupId) {
@ -7507,13 +7474,6 @@ class App extends React.Component<AppProps, AppState> {
imageFile: File,
showCursorImagePreview?: boolean,
) => {
// we should be handling all cases upstream, but in case we forget to handle
// a future case, let's throw here
if (!this.isToolSupported("image")) {
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
return;
}
this.scene.addNewElement(imageElement);
try {
@ -7597,7 +7557,6 @@ class App extends React.Component<AppProps, AppState> {
const imageElement = this.createImageElement({
sceneX: x,
sceneY: y,
addToFrameUnderCursor: false,
});
if (insertOnCanvasDirectly) {
@ -7898,10 +7857,7 @@ class App extends React.Component<AppProps, AppState> {
);
try {
// if image tool not supported, don't show an error here and let it fall
// through so we still support importing scene data from images. If no
// scene data encoded, we'll show an error then
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
if (isSupportedImageFile(file)) {
// first attempt to decode scene from the image if it's embedded
// ---------------------------------------------------------------------
@ -8029,17 +7985,6 @@ class App extends React.Component<AppProps, AppState> {
});
}
} catch (error: any) {
if (
error instanceof ImageSceneDataError &&
error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
!this.isToolSupported("image")
) {
this.setState({
isLoading: false,
errorMessage: t("errors.imageToolNotSupported"),
});
return;
}
this.setState({ isLoading: false, errorMessage: error.message });
}
};

View File

@ -280,7 +280,6 @@ const LayerUI = ({
<ShapesSwitcher
appState={appState}
activeTool={appState.activeTool}
UIOptions={UIOptions}
app={app}
/>
</Stack.Row>
@ -471,7 +470,6 @@ const LayerUI = ({
renderSidebars={renderSidebars}
device={device}
renderWelcomeScreen={renderWelcomeScreen}
UIOptions={UIOptions}
/>
)}
{!device.editor.isMobile && (

View File

@ -1,7 +1,6 @@
import React from "react";
import {
AppClassProperties,
AppProps,
AppState,
Device,
ExcalidrawProps,
@ -46,7 +45,6 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
UIOptions: AppProps["UIOptions"];
app: AppClassProperties;
};
@ -64,7 +62,6 @@ export const MobileMenu = ({
renderSidebars,
device,
renderWelcomeScreen,
UIOptions,
app,
}: MobileMenuProps) => {
const {
@ -86,7 +83,6 @@ export const MobileMenu = ({
<ShapesSwitcher
appState={appState}
activeTool={appState.activeTool}
UIOptions={UIOptions}
app={app}
/>
</Stack.Row>

View File

@ -222,9 +222,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
toggleTheme: null,
saveAsImage: true,
},
tools: {
image: true,
},
};
// breakpoints

View File

@ -99,7 +99,7 @@ export const setCursorForShape = (
interactiveCanvas.style.cursor = `url(${url}), auto`;
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
} else if (appState.activeTool.type !== "image") {
} else {
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
}
};

View File

@ -14,7 +14,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -50,7 +49,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -81,7 +79,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "ellipse-1",
@ -135,7 +132,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "ellipse-1",
@ -194,7 +190,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -232,7 +227,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -277,7 +271,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -320,7 +313,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "text-2",
@ -376,7 +368,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"baseline": 0,
"boundElements": null,
"containerId": "id48",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -419,7 +410,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id40",
@ -475,7 +465,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"baseline": 0,
"boundElements": null,
"containerId": "id37",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -518,7 +507,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -554,7 +542,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -590,7 +577,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id44",
@ -646,7 +632,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"baseline": 0,
"boundElements": null,
"containerId": "id41",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -691,7 +676,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -736,7 +720,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -774,7 +757,6 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -805,7 +787,6 @@ exports[`Test Transform > should transform linear elements 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -851,7 +832,6 @@ exports[`Test Transform > should transform linear elements 2`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "triangle",
"endBinding": null,
"fillStyle": "solid",
@ -897,7 +877,6 @@ exports[`Test Transform > should transform linear elements 3`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -943,7 +922,6 @@ exports[`Test Transform > should transform linear elements 4`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -989,7 +967,6 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1020,7 +997,6 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1051,7 +1027,6 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1082,7 +1057,6 @@ exports[`Test Transform > should transform regular shapes 4`] = `
"angle": 0,
"backgroundColor": "#c0eb75",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1113,7 +1087,6 @@ exports[`Test Transform > should transform regular shapes 5`] = `
"angle": 0,
"backgroundColor": "#ffc9c9",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1144,7 +1117,6 @@ exports[`Test Transform > should transform regular shapes 6`] = `
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -1177,7 +1149,6 @@ exports[`Test Transform > should transform text element 1`] = `
"baseline": 0,
"boundElements": null,
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1217,7 +1188,6 @@ exports[`Test Transform > should transform text element 2`] = `
"baseline": 0,
"boundElements": null,
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1260,7 +1230,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1311,7 +1280,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1362,7 +1330,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1413,7 +1380,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text",
},
],
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1461,7 +1427,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id25",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1501,7 +1466,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id26",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1541,7 +1505,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id27",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1582,7 +1545,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id28",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1626,7 +1588,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1662,7 +1623,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1698,7 +1658,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1734,7 +1693,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1770,7 +1728,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1806,7 +1763,6 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1839,7 +1795,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id13",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1879,7 +1834,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id14",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1920,7 +1874,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id15",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -1963,7 +1916,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id16",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -2004,7 +1956,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id17",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,
@ -2046,7 +1997,6 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0,
"boundElements": null,
"containerId": "id18",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 20,

View File

@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState";
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError, ImageSceneDataError } from "../errors";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types";
@ -24,12 +24,15 @@ const parseFileContents = async (blob: Blob | File) => {
).decodePngMetadata(blob);
} catch (error: any) {
if (error.message === "INVALID") {
throw new ImageSceneDataError(
throw new DOMException(
t("alerts.imageDoesNotContainScene"),
"IMAGE_NOT_CONTAINS_SCENE_DATA",
"EncodingError",
);
} else {
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
}
}
} else {
@ -55,12 +58,15 @@ const parseFileContents = async (blob: Blob | File) => {
});
} catch (error: any) {
if (error.message === "INVALID") {
throw new ImageSceneDataError(
throw new DOMException(
t("alerts.imageDoesNotContainScene"),
"IMAGE_NOT_CONTAINS_SCENE_DATA",
"EncodingError",
);
} else {
throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage"));
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
}
}
}
@ -125,19 +131,8 @@ export const loadSceneOrLibraryFromBlob = async (
fileHandle?: FileSystemHandle | null,
) => {
const contents = await parseFileContents(blob);
let data;
try {
try {
data = JSON.parse(contents);
} catch (error: any) {
if (isSupportedImageFile(blob)) {
throw new ImageSceneDataError(
t("alerts.imageDoesNotContainScene"),
"IMAGE_NOT_CONTAINS_SCENE_DATA",
);
}
throw error;
}
const data = JSON.parse(contents);
if (isValidExcalidrawData(data)) {
return {
type: MIME_TYPES.excalidraw,
@ -167,9 +162,7 @@ export const loadSceneOrLibraryFromBlob = async (
}
throw new Error(t("alerts.couldNotLoadInvalidFile"));
} catch (error: any) {
if (error instanceof ImageSceneDataError) {
throw error;
}
console.error(error.message);
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
};

View File

@ -822,22 +822,4 @@ describe("Test Transform", () => {
"Duplicate id found for rect-1",
);
});
it("should contains customData if provided", () => {
const rawData = [
{
type: "rectangle",
x: 100,
y: 100,
customData: { createdBy: "user01" },
},
];
const convertedElements = convertToExcalidrawElements(
rawData as ExcalidrawElementSkeleton[],
opts,
);
expect(convertedElements[0].customData).toStrictEqual({
createdBy: "user01",
});
});
});

View File

@ -1,15 +1,11 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
export const sanitizeHTMLAttribute = (html: string) => {
return html.replace(/"/g, "&quot;");
};
export const normalizeLink = (link: string) => {
link = link.trim();
if (!link) {
return link;
}
return sanitizeUrl(sanitizeHTMLAttribute(link));
return sanitizeUrl(link);
};
export const isLocalLink = (link: string | null) => {

View File

@ -13,7 +13,6 @@ import { Point } from "../types";
import { generateRoughOptions } from "../scene/Shape";
import {
isArrowElement,
isBoundToContainer,
isFreeDrawElement,
isLinearElement,
isTextElement,
@ -23,7 +22,6 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene";
export type RectangleBox = {
x: number;
@ -55,29 +53,16 @@ export class ElementBounds {
static getBounds(element: ExcalidrawElement) {
const cachedBounds = ElementBounds.boundsCache.get(element);
if (
cachedBounds?.version &&
cachedBounds.version === element.version &&
// we don't invalidate cache when we update containers and not labels,
// which is causing problems down the line. Fix TBA.
!isBoundToContainer(element)
) {
if (cachedBounds?.version && cachedBounds.version === element.version) {
return cachedBounds.bounds;
}
const bounds = ElementBounds.calculateBounds(element);
// hack to ensure that downstream checks could retrieve element Scene
// so as to have correctly calculated bounds
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
const shouldCache = Scene.getScene(element);
if (shouldCache) {
ElementBounds.boundsCache.set(element, {
version: element.version,
bounds,
});
}
ElementBounds.boundsCache.set(element, {
version: element.version,
bounds,
});
return bounds;
}

View File

@ -13,13 +13,11 @@ import {
NonDeletedExcalidrawElement,
Theme,
} from "./types";
import { sanitizeHTMLAttribute } from "../data/url";
type EmbeddedLink =
| ({
aspectRatio: { w: number; h: number };
warning?: string;
sandbox?: { allowSameOrigin?: boolean };
} & (
| { type: "video" | "generic"; link: string }
| { type: "document"; srcdoc: (theme: Theme) => string }
@ -32,21 +30,20 @@ const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
const RE_GH_GIST = /^https:\/\/gist\.github\.com/;
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
const RE_TWITTER =
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
const RE_TWITTER_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i;
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
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_$]+/;
const RE_GENERIC_EMBED =
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
@ -146,46 +143,56 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
}
if (RE_TWITTER.test(link)) {
const postId = link.match(RE_TWITTER)![1];
// the embed srcdoc still supports twitter.com domain only.
// Note that we don't attempt to parse the username as it can consist of
// non-latin1 characters, and the username in the url can be set to anything
// without affecting the embed.
const safeURL = sanitizeHTMLAttribute(
`https://twitter.com/x/status/${postId}`,
);
const ret: EmbeddedLink = {
type: "document",
srcdoc: (theme: string) =>
createSrcDoc(
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${safeURL}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
),
aspectRatio: { w: 480, h: 480 },
sandbox: { allowSameOrigin: true },
};
let ret: EmbeddedLink;
// assume embed code
if (/<blockquote/.test(link)) {
const srcDoc = createSrcDoc(link);
ret = {
type: "document",
srcdoc: () => srcDoc,
aspectRatio: { w: 480, h: 480 },
};
// assume regular tweet url
} else {
ret = {
type: "document",
srcdoc: (theme: string) =>
createSrcDoc(
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
),
aspectRatio: { w: 480, h: 480 },
};
}
embeddedLinkCache.set(originalLink, ret);
return ret;
}
if (RE_GH_GIST.test(link)) {
const [, user, gistId] = link.match(RE_GH_GIST)!;
const safeURL = sanitizeHTMLAttribute(
`https://gist.github.com/${user}/${gistId}`,
);
const ret: EmbeddedLink = {
type: "document",
srcdoc: () =>
createSrcDoc(`
<script src="${safeURL}.js"></script>
let ret: EmbeddedLink;
// assume embed code
if (/<script>/.test(link)) {
const srcDoc = createSrcDoc(link);
ret = {
type: "document",
srcdoc: () => srcDoc,
aspectRatio: { w: 550, h: 720 },
};
// assume regular url
} else {
ret = {
type: "document",
srcdoc: () =>
createSrcDoc(`
<script src="${link}.js"></script>
<style type="text/css">
* { margin: 0px; }
table, .gist { height: 100%; }
.gist .gist-file { height: calc(100vh - 2px); padding: 0px; display: grid; grid-template-rows: 1fr auto; }
</style>
`),
aspectRatio: { w: 550, h: 720 },
};
aspectRatio: { w: 550, h: 720 },
};
}
embeddedLinkCache.set(link, ret);
return ret;
}

View File

@ -67,7 +67,6 @@ export type ElementConstructorOpts = MarkOptional<
| "roundness"
| "locked"
| "opacity"
| "customData"
>;
const _newElementBase = <T extends ExcalidrawElement>(
@ -121,7 +120,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
updated: getUpdatedTimestamp(),
link,
locked,
customData: rest.customData,
};
return element;
};

View File

@ -16,19 +16,3 @@ export class AbortError extends DOMException {
super(message, "AbortError");
}
}
type ImageSceneDataErrorCode =
| "IMAGE_NOT_CONTAINS_SCENE_DATA"
| "IMAGE_SCENE_DATA_ERROR";
export class ImageSceneDataError extends Error {
public code;
constructor(
message = "Image Scene Data Error",
code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR",
) {
super(message);
this.name = "EncodingError";
this.code = code;
}
}

View File

@ -1,8 +1,4 @@
import {
getCommonBounds,
getElementAbsoluteCoords,
isTextElement,
} from "./element";
import { getCommonBounds, getElementBounds, isTextElement } from "./element";
import {
ExcalidrawElement,
ExcalidrawFrameElement,
@ -56,6 +52,7 @@ export const bindElementsToFramesAfterDuplication = (
}
};
// --------------------------- Frame Geometry ---------------------------------
export function isElementIntersectingFrame(
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
@ -85,36 +82,27 @@ export const getElementsCompletelyInFrame = (
element.frameId === frame.id,
);
export const isElementContainingFrame = (
elements: readonly ExcalidrawElement[],
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
) => {
return getElementsWithinSelection(elements, element).some(
(e) => e.id === frame.id,
);
};
export const getElementsIntersectingFrame = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement,
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
export const elementsAreInFrameBounds = (
export const elementsAreInBounds = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement,
element: ExcalidrawElement,
tolerance = 0,
) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(frame);
const [elementX1, elementY1, elementX2, elementY2] =
getElementBounds(element);
const [elementsX1, elementsY1, elementsX2, elementsY2] =
getCommonBounds(elements);
return (
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2
elementX1 <= elementsX1 - tolerance &&
elementY1 <= elementsY1 - tolerance &&
elementX2 >= elementsX2 + tolerance &&
elementY2 >= elementsY2 + tolerance
);
};
@ -123,9 +111,12 @@ export const elementOverlapsWithFrame = (
frame: ExcalidrawFrameElement,
) => {
return (
elementsAreInFrameBounds([element], frame) ||
isElementIntersectingFrame(element, frame) ||
isElementContainingFrame([frame], element, frame)
// frame contains element
elementsAreInBounds([element], frame) ||
// element contains frame
(elementsAreInBounds([frame], element) && element.frameId === frame.id) ||
// element intersects with frame
isElementIntersectingFrame(element, frame)
);
};
@ -136,7 +127,7 @@ export const isCursorInFrame = (
},
frame: NonDeleted<ExcalidrawFrameElement>,
) => {
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
const [fx1, fy1, fx2, fy2] = getElementBounds(frame);
return isPointWithinBounds(
[fx1, fy1],
@ -160,7 +151,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
return !!elementsInGroup.find(
(element) =>
elementsAreInFrameBounds([element], frame) ||
elementsAreInBounds([element], frame) ||
isElementIntersectingFrame(element, frame),
);
};
@ -181,7 +172,7 @@ export const groupsAreCompletelyOutOfFrame = (
return (
elementsInGroup.find(
(element) =>
elementsAreInFrameBounds([element], frame) ||
elementsAreInBounds([element], frame) ||
isElementIntersectingFrame(element, frame),
) === undefined
);
@ -249,12 +240,18 @@ export const getElementsInResizingFrame = (
const prevElementsInFrame = getFrameChildren(allElements, frame.id);
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
const elementsCompletelyInFrame = new Set([
...getElementsCompletelyInFrame(allElements, frame),
...prevElementsInFrame.filter((element) =>
isElementContainingFrame(allElements, element, frame),
),
]);
const elementsCompletelyInFrame = new Set<ExcalidrawElement>(
getElementsCompletelyInFrame(allElements, frame),
);
for (const element of prevElementsInFrame) {
if (!elementsCompletelyInFrame.has(element)) {
// element contains the frame
if (elementsAreInBounds([frame], element)) {
elementsCompletelyInFrame.add(element);
}
}
}
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
(element) => !elementsCompletelyInFrame.has(element),
@ -321,7 +318,7 @@ export const getElementsInResizingFrame = (
if (isSelected) {
const elementsInGroup = getElementsInGroup(allElements, id);
if (elementsAreInFrameBounds(elementsInGroup, frame)) {
if (elementsAreInBounds(elementsInGroup, frame)) {
for (const element of elementsInGroup) {
nextElementsInFrame.add(element);
}
@ -370,7 +367,7 @@ export const getContainingFrame = (
// --------------------------- Frame Operations -------------------------------
/**
* Retains (or repairs for target frame) the ordering invriant where children
* Retains (or repairs for target frame) the ordering invariant where children
* elements come right before the parent frame:
* [el, el, child, child, frame, el]
*/
@ -437,25 +434,14 @@ export const removeElementsFromFrame = (
ExcalidrawElement
>();
const toRemoveElementsByFrame = new Map<
ExcalidrawFrameElement["id"],
ExcalidrawElement[]
>();
for (const element of elementsToRemove) {
if (element.frameId) {
_elementsToRemove.set(element.id, element);
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
arr.push(element);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
_elementsToRemove.set(boundTextElement.id, boundTextElement);
arr.push(boundTextElement);
}
toRemoveElementsByFrame.set(element.frameId, arr);
}
}
@ -520,12 +506,15 @@ export const updateFrameMembershipOfSelectedElements = (
}
const elementsToRemove = new Set<ExcalidrawElement>();
const processedGroupIds = new Map<string, boolean>();
elementsToFilter.forEach((element) => {
if (
element.frameId &&
!isFrameElement(element) &&
!isElementInFrame(element, allElements, appState)
!isElementInFrame(element, allElements, appState, {
processedGroupIds,
})
) {
elementsToRemove.add(element);
}
@ -587,26 +576,36 @@ export const getTargetFrame = (
: getContainingFrame(_element);
};
// TODO: this a huge bottleneck for large scenes, optimise
// given an element, return if the element is in some frame
export const isElementInFrame = (
element: ExcalidrawElement,
allElements: ExcalidrawElementsIncludingDeleted,
appState: StaticCanvasAppState,
opts?: {
targetFrame?: ExcalidrawFrameElement;
processedGroupIds?: Map<string, boolean>;
},
) => {
const frame = getTargetFrame(element, appState);
const frame = opts?.targetFrame ?? getTargetFrame(element, appState);
const _element = isTextElement(element)
? getContainerElement(element) || element
: element;
const groupsInFrame = (yes: boolean) => {
if (opts?.processedGroupIds) {
_element.groupIds.forEach((gid) => {
opts.processedGroupIds?.set(gid, yes);
});
}
};
if (frame) {
// Perf improvement:
// For an element that's already in a frame, if it's not being dragged
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
// It has to be in its containing frame.
// For an element that's already in a frame, if it's not being selected
// and its frame is not being selected, it has to be in its containing frame.
if (
!appState.selectedElementIds[element.id] ||
!appState.selectedElementsAreBeingDragged
!appState.selectedElementIds[element.id] &&
!appState.selectedElementIds[frame.id]
) {
return true;
}
@ -615,8 +614,21 @@ export const isElementInFrame = (
return elementOverlapsWithFrame(_element, frame);
}
for (const gid of _element.groupIds) {
if (opts?.processedGroupIds?.has(gid)) {
return opts.processedGroupIds.get(gid);
}
}
const allElementsInGroup = new Set(
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
_element.groupIds
.filter((gid) => {
if (opts?.processedGroupIds) {
return !opts.processedGroupIds.has(gid);
}
return true;
})
.flatMap((gid) => getElementsInGroup(allElements, gid)),
);
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
@ -637,16 +649,22 @@ export const isElementInFrame = (
for (const elementInGroup of allElementsInGroup) {
if (isFrameElement(elementInGroup)) {
groupsInFrame(false);
return false;
}
}
for (const elementInGroup of allElementsInGroup) {
if (elementOverlapsWithFrame(elementInGroup, frame)) {
groupsInFrame(true);
return true;
}
}
}
if (_element.groupIds.length > 0) {
groupsInFrame(false);
}
return false;
};

View File

@ -232,6 +232,8 @@ export const selectGroupsFromGivenElements = (
selectedGroupIds: {},
};
const processedGroupIds = new Set<string>();
for (const element of elements) {
let groupIds = element.groupIds;
if (appState.editingGroupId) {
@ -242,10 +244,13 @@ export const selectGroupsFromGivenElements = (
}
if (groupIds.length > 0) {
const groupId = groupIds[groupIds.length - 1];
nextAppState = {
...nextAppState,
...selectGroup(groupId, nextAppState, elements),
};
if (!processedGroupIds.has(groupId)) {
nextAppState = {
...nextAppState,
...selectGroup(groupId, nextAppState, elements),
};
processedGroupIds.add(groupId);
}
}
}

View File

@ -209,7 +209,6 @@
"importLibraryError": "Couldn't load library",
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"imageToolNotSupported": "Images are disabled.",
"brave_measure_text_error": {
"line1": "Looks like you are using Brave browser with the <bold>Aggressively Block Fingerprinting</bold> setting enabled.",
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",

View File

@ -1,7 +1,7 @@
[
{
"path": "dist/excalidraw.production.min.js",
"limit": "335 kB"
"limit": "325 kB"
},
{
"path": "dist/excalidraw-assets/locales",

View File

@ -15,84 +15,14 @@ Please add the latest change on the top under the correct section.
### 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)
### Fixes
- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656)
### 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)
## 0.17.1 (2023-11-28)
### Fixes
- Umd build for browser since it was breaking in v0.17.0 [#7349](https://github.com/excalidraw/excalidraw/pull/7349). Also make sure that when using `Vite`, the `process.env.IS_PREACT` is set as `"true"` (string) and not a boolean.
```
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
}
```
### Breaking Changes
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
## 0.17.0 (2023-11-14)
### Features
- Added support for disabling `image` tool (also disabling image insertion in general, though keeps support for importing from `.excalidraw` files) [#6320](https://github.com/excalidraw/excalidraw/pull/6320).
For disabling `image` you need to set 👇
```
UIOptions.tools = {
image: false
}
```
- Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
#### BREAKING CHANGE
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api)
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0.
- Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247).
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205).
@ -101,156 +31,12 @@ define: {
- Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195)
- Add onChange, onPointerDown, onPointerUp api subscribers [#7154](https://github.com/excalidraw/excalidraw/pull/7154).
- Support props.locked for setActiveTool [#7153](https://github.com/excalidraw/excalidraw/pull/7153).
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078)
### Fixes
- Double image dialog on image insertion [#7152](https://github.com/excalidraw/excalidraw/pull/7152).
### Breaking Changes
- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api) [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0 [#7251](https://github.com/excalidraw/excalidraw/pull/7251).
#### BREAKING CHANGES
- [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243)
### Build
- Support Preact [#7255](https://github.com/excalidraw/excalidraw/pull/7255). The host needs to set `process.env.IS_PREACT` to `true`
When using `vite` or any build tools, you will have to make sure the `process` is accessible as we are accessing `process.env.IS_PREACT` to decide whether to use the `preact` build.
Since `Vite` removes env variables by default, you can update the Vite config to ensure its available :point_down:
```
define: {
"process.env.IS_PREACT": process.env.IS_PREACT,
},
```
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Features
- Allow D&D dice app domain for embeds [#7263](https://github.com/excalidraw/excalidraw/pull/7263)
- Remove full screen shortcut [#7222](https://github.com/excalidraw/excalidraw/pull/7222)
- Make adaptive-roughness less aggressive [#7250](https://github.com/excalidraw/excalidraw/pull/7250)
- Render frames on export [#7210](https://github.com/excalidraw/excalidraw/pull/7210)
- Support mermaid flowchart and sequence diagrams to excalidraw diagrams 🥳 [#6920](https://github.com/excalidraw/excalidraw/pull/6920)
- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205)
- Make clipboard more robust and reintroduce contextmenu actions [#7198](https://github.com/excalidraw/excalidraw/pull/7198)
- Support giphy.com embed domain [#7192](https://github.com/excalidraw/excalidraw/pull/7192)
- Renderer tweaks [#6698](https://github.com/excalidraw/excalidraw/pull/6698)
- Closing of "Save to.." Dialog on Save To Disk [#7168](https://github.com/excalidraw/excalidraw/pull/7168)
- Added Copy/Paste from Google Docs [#7136](https://github.com/excalidraw/excalidraw/pull/7136)
- Remove bound-arrows from frames [#7157](https://github.com/excalidraw/excalidraw/pull/7157)
- New dark mode theme & light theme tweaks [#7104](https://github.com/excalidraw/excalidraw/pull/7104)
- Better laser cursor for dark mode [#7132](https://github.com/excalidraw/excalidraw/pull/7132)
- Laser pointer improvements [#7128](https://github.com/excalidraw/excalidraw/pull/7128)
- Initial Laser Pointer MVP [#6739](https://github.com/excalidraw/excalidraw/pull/6739)
- Export `iconFillColor()` [#6996](https://github.com/excalidraw/excalidraw/pull/6996)
- Element alignments - snapping [#6256](https://github.com/excalidraw/excalidraw/pull/6256)
### Fixes
- Image insertion bugs [#7278](https://github.com/excalidraw/excalidraw/pull/7278)
- ExportToSvg to honor frameRendering also for name not only for frame itself [#7270](https://github.com/excalidraw/excalidraw/pull/7270)
- Can't toggle penMode off due to missing typecheck in togglePenMode [#7273](https://github.com/excalidraw/excalidraw/pull/7273)
- Replace hard coded font family with const value in addFrameLabelsAsTextElements [#7269](https://github.com/excalidraw/excalidraw/pull/7269)
- Perf issue when ungrouping elements within frame [#7265](https://github.com/excalidraw/excalidraw/pull/7265)
- Fixes the shortcut collision between "toggleHandTool" and "distributeHorizontally" [#7189](https://github.com/excalidraw/excalidraw/pull/7189)
- Allow pointer events when editing a linear element [#7238](https://github.com/excalidraw/excalidraw/pull/7238)
- Make modal use viewport breakpoints [#7246](https://github.com/excalidraw/excalidraw/pull/7246)
- Align input `:hover`/`:focus` with spec [#7225](https://github.com/excalidraw/excalidraw/pull/7225)
- Dialog remounting on className updates [#7224](https://github.com/excalidraw/excalidraw/pull/7224)
- Don't update label position when dragging labelled arrows [#6891](https://github.com/excalidraw/excalidraw/pull/6891)
- Frame add/remove/z-index ordering changes [#7194](https://github.com/excalidraw/excalidraw/pull/7194)
- Element relative position when dragging multiple elements on grid [#7107](https://github.com/excalidraw/excalidraw/pull/7107)
- Freedraw non-solid bg hitbox not working [#7193](https://github.com/excalidraw/excalidraw/pull/7193)
- Actions panel ux improvement [#6850](https://github.com/excalidraw/excalidraw/pull/6850)
- Better fill rendering with latest RoughJS [#7031](https://github.com/excalidraw/excalidraw/pull/7031)
- Fix for Strange Symbol Appearing on Canvas after Deleting Grouped Graphics (Issue #7116) [#7170](https://github.com/excalidraw/excalidraw/pull/7170)
- Attempt to fix flake in wysiwyg tests [#7173](https://github.com/excalidraw/excalidraw/pull/7173)
- Ensure `ClipboardItem` created in the same tick to fix safari [#7066](https://github.com/excalidraw/excalidraw/pull/7066)
- Wysiwyg left in undefined state on reload [#7123](https://github.com/excalidraw/excalidraw/pull/7123)
- Ensure relative z-index of elements added to frame is retained [#7134](https://github.com/excalidraw/excalidraw/pull/7134)
- Memoize static canvas on `props.renderConfig` [#7131](https://github.com/excalidraw/excalidraw/pull/7131)
- Regression from #6739 preventing redirect link in view mode [#7120](https://github.com/excalidraw/excalidraw/pull/7120)
- Update links to excalidraw-app [#7072](https://github.com/excalidraw/excalidraw/pull/7072)
- Ensure we do not stop laser update prematurely [#7100](https://github.com/excalidraw/excalidraw/pull/7100)
- Remove invisible elements safely [#7083](https://github.com/excalidraw/excalidraw/pull/7083)
- Icon size in manifest [#7073](https://github.com/excalidraw/excalidraw/pull/7073)
- Elements being dropped/duplicated when added to frame [#7057](https://github.com/excalidraw/excalidraw/pull/7057)
- Frame name not editable on dbl-click [#7037](https://github.com/excalidraw/excalidraw/pull/7037)
- Polyfill `Element.replaceChildren` [#7034](https://github.com/excalidraw/excalidraw/pull/7034)
### Refactor
- DRY out tool typing [#7086](https://github.com/excalidraw/excalidraw/pull/7086)
- Refactor event globals to differentiate from `lastPointerUp` [#7084](https://github.com/excalidraw/excalidraw/pull/7084)
- DRY out and simplify setting active tool from toolbar [#7079](https://github.com/excalidraw/excalidraw/pull/7079)
### Performance
- Improve element in frame check [#7124](https://github.com/excalidraw/excalidraw/pull/7124)
---
## 0.16.1 (2023-09-21)
## Excalidraw Library
@ -272,7 +58,7 @@ define: {
- 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)
- 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/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
- 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)
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
@ -551,7 +337,7 @@ define: {
### Features
- [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
- [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)

View File

@ -98,7 +98,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
const [exportEmbedScene, setExportEmbedScene] = useState(false);
const [theme, setTheme] = useState<Theme>("light");
const [disableImageTool, setDisableImageTool] = useState(false);
const [isCollaborating, setIsCollaborating] = useState(false);
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
{},
@ -607,16 +606,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
/>
Switch to Dark Theme
</label>
<label>
<input
type="checkbox"
checked={disableImageTool === true}
onChange={() => {
setDisableImageTool(!disableImageTool);
}}
/>
Disable Image Tool
</label>
<label>
<input
type="checkbox"
@ -697,7 +686,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
canvasActions: {
loadScene: false,
},
tools: { image: !disableImageTool },
}}
renderTopRightUI={renderTopRightUI}
onLinkOpen={onLinkOpen}

View File

@ -56,9 +56,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions,
},
tools: {
image: props.UIOptions?.tools?.image ?? true,
},
};
if (canvasActions?.export) {

View File

@ -1,10 +1,4 @@
if (process.env.IS_PREACT === "true") {
if (process.env.NODE_ENV === "production") {
module.exports = require("./dist/excalidraw-with-preact.production.min.js");
} else {
module.exports = require("./dist/excalidraw-with-preact.development.js");
}
} else if (process.env.NODE_ENV === "production") {
if (process.env.NODE_ENV === "production") {
module.exports = require("./dist/excalidraw.production.min.js");
} else {
module.exports = require("./dist/excalidraw.development.js");

View File

@ -1,6 +1,6 @@
{
"name": "@excalidraw/excalidraw",
"version": "0.17.5",
"version": "0.16.1",
"main": "main.js",
"types": "types/packages/excalidraw/index.d.ts",
"files": [
@ -78,7 +78,7 @@
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
"scripts": {
"gen:types": "tsc --project ../../../tsconfig-types.json",
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && NODE_ENV=development webpack --config webpack.preact.config.js && NODE_ENV=production webpack --config webpack.preact.config.js && yarn gen:types",
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack",
"start": "webpack serve --config webpack.dev-server.config.js",

View File

@ -1,32 +0,0 @@
const prodConfig = require("./webpack.prod.config");
const devConfig = require("./webpack.dev.config");
const isProd = process.env.NODE_ENV === "production";
const config = isProd ? prodConfig : devConfig;
const outputFile = isProd
? "excalidraw-with-preact.production.min"
: "excalidraw-with-preact.development";
const preactWebpackConfig = {
...config,
entry: {
[outputFile]: "./entry.js",
},
externals: {
...config.externals,
"react-dom/client": {
root: "ReactDOMClient",
commonjs2: "react-dom/client",
commonjs: "react-dom/client",
amd: "react-dom/client",
},
"react/jsx-runtime": {
root: "ReactJSXRuntime",
commonjs2: "react/jsx-runtime",
commonjs: "react/jsx-runtime",
amd: "react/jsx-runtime",
},
},
};
module.exports = preactWebpackConfig;

View File

@ -71,6 +71,7 @@ import { renderSnaps } from "./renderSnaps";
import {
isEmbeddableElement,
isFrameElement,
isFreeDrawElement,
isLinearElement,
} from "../element/typeChecks";
import {
@ -78,7 +79,7 @@ import {
createPlaceholderEmbeddableLabel,
} from "../element/embeddable";
import {
elementOverlapsWithFrame,
elementsAreInBounds,
getTargetFrame,
isElementInFrame,
} from "../frame";
@ -945,102 +946,79 @@ const _renderStaticScene = ({
);
}
const groupsToBeAddedToFrame = new Set<string>();
// Paint visible elements with embeddables on top
const visibleNonEmbeddableOrLabelElements = visibleElements.filter(
(el) => !isEmbeddableOrLabel(el),
);
visibleElements.forEach((element) => {
if (
element.groupIds.length > 0 &&
appState.frameToHighlight &&
appState.selectedElementIds[element.id] &&
(elementOverlapsWithFrame(element, appState.frameToHighlight) ||
element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
) {
element.groupIds.forEach((groupId) =>
groupsToBeAddedToFrame.add(groupId),
);
const visibleEmbeddableOrLabelElements = visibleElements.filter((el) =>
isEmbeddableOrLabel(el),
);
const visibleElementsToRender = [
...visibleNonEmbeddableOrLabelElements,
...visibleEmbeddableOrLabelElements,
];
const _renderElement = (element: ExcalidrawElement) => {
try {
renderElement(element, rc, context, renderConfig, appState);
if (
isEmbeddableElement(element) &&
(isExporting || !element.validated) &&
element.width &&
element.height
) {
const label = createPlaceholderEmbeddableLabel(element);
renderElement(label, rc, context, renderConfig, appState);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
}
} catch (error: any) {
console.error(error);
}
});
};
// Paint visible elements
visibleElements
.filter((el) => !isEmbeddableOrLabel(el))
.forEach((element) => {
try {
const frameId = element.frameId || appState.frameToHighlight?.id;
const processedGroupIds = new Map<string, boolean>();
for (const element of visibleElementsToRender) {
const frameId = element.frameId || appState.frameToHighlight?.id;
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
context.save();
const frame = getTargetFrame(element, appState);
// TODO do we need to check isElementInFrame here?
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig, appState);
}
renderElement(element, rc, context, renderConfig, appState);
context.restore();
} else {
renderElement(element, rc, context, renderConfig, appState);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
}
} catch (error: any) {
console.error(error);
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const targetFrame = getTargetFrame(element, appState);
// for perf:
// only clip elements that are not completely in the target frame
if (
targetFrame &&
!elementsAreInBounds(
[element],
targetFrame,
isFreeDrawElement(element)
? element.strokeWidth * 8
: element.roughness * (isLinearElement(element) ? 8 : 4),
) &&
isElementInFrame(element, elements, appState, {
targetFrame,
processedGroupIds,
})
) {
context.save();
frameClip(targetFrame, context, renderConfig, appState);
_renderElement(element);
context.restore();
} else {
_renderElement(element);
}
});
// render embeddables on top
visibleElements
.filter((el) => isEmbeddableOrLabel(el))
.forEach((element) => {
try {
const render = () => {
renderElement(element, rc, context, renderConfig, appState);
if (
isEmbeddableElement(element) &&
(isExporting || !element.validated) &&
element.width &&
element.height
) {
const label = createPlaceholderEmbeddableLabel(element);
renderElement(label, rc, context, renderConfig, appState);
}
if (!isExporting) {
renderLinkIcon(element, context, appState);
}
};
// - when exporting the whole canvas, we DO NOT apply clipping
// - when we are exporting a particular frame, apply clipping
// if the containing frame is not selected, apply clipping
const frameId = element.frameId || appState.frameToHighlight?.id;
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
context.save();
const frame = getTargetFrame(element, appState);
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig, appState);
}
render();
context.restore();
} else {
render();
}
} catch (error: any) {
console.error(error);
}
});
} else {
_renderElement(element);
}
}
};
/** throttled to animation framerate */
@ -1145,7 +1123,7 @@ const renderTransformHandles = (
const renderSelectionBorder = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
appState: InteractiveCanvasAppState | StaticCanvasAppState,
elementProperties: {
angle: number;
elementX1: number;
@ -1310,6 +1288,23 @@ const renderFrameHighlight = (
context.restore();
};
const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return {
angle: 0,
elementX1,
elementX2,
elementY1,
elementY2,
selectionColors: ["rgb(0,118,255)"],
dashed: false,
cx: elementX1 + (elementX2 - elementX1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2,
activeEmbeddable: false,
};
};
const renderElementsBoxHighlight = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
@ -1323,37 +1318,28 @@ const renderElementsBoxHighlight = (
(element) => element.groupIds.length > 0,
);
const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return {
angle: 0,
elementX1,
elementX2,
elementY1,
elementY2,
selectionColors: ["rgb(0,118,255)"],
dashed: false,
cx: elementX1 + (elementX2 - elementX1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2,
activeEmbeddable: false,
};
};
const processedGroupIds = new Set<string>();
const getSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elements, groupId);
return getSelectionFromElements(groupElements);
if (!processedGroupIds.has(groupId)) {
const groupElements = getElementsInGroup(elements, groupId);
processedGroupIds.add(groupId);
return getSelectionFromElements(groupElements);
}
return null;
};
Object.entries(selectGroupsFromGivenElements(elementsInGroups, appState))
.filter(([id, isSelected]) => isSelected)
.map(([id, isSelected]) => id)
.map((groupId) => getSelectionForGroupId(groupId))
.filter((selection) => selection)
.concat(
individualElements.map((element) => getSelectionFromElements([element])),
)
.forEach((selection) =>
renderSelectionBorder(context, appState, selection),
renderSelectionBorder(context, appState, selection!),
);
};

View File

@ -385,7 +385,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"angle": 0,
"backgroundColor": "red",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -420,7 +419,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"angle": 0,
"backgroundColor": "red",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -582,7 +580,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -642,7 +639,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -784,7 +780,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -817,7 +812,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -877,7 +871,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -921,7 +914,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -951,7 +943,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -995,7 +986,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1025,7 +1015,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1167,7 +1156,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1200,7 +1188,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1260,7 +1247,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1304,7 +1290,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1334,7 +1319,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1378,7 +1362,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1408,7 +1391,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1552,7 +1534,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1612,7 +1593,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1752,7 +1732,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1812,7 +1791,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1854,7 +1832,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -1996,7 +1973,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2029,7 +2005,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2089,7 +2064,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2133,7 +2107,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2163,7 +2136,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2310,7 +2282,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -2345,7 +2316,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -2407,7 +2377,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2451,7 +2420,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2481,7 +2449,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2528,7 +2495,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -2560,7 +2526,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -2706,7 +2671,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -2739,7 +2703,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -2799,7 +2762,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2843,7 +2805,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2873,7 +2834,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2917,7 +2877,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2947,7 +2906,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -2991,7 +2949,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3021,7 +2978,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3065,7 +3021,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3095,7 +3050,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3139,7 +3093,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3169,7 +3122,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3213,7 +3165,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3243,7 +3194,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3287,7 +3237,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3317,7 +3266,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3361,7 +3309,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3391,7 +3338,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0,
"backgroundColor": "#a5d8ff",
"boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [],
@ -3533,7 +3479,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3566,7 +3511,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3626,7 +3570,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3670,7 +3613,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3700,7 +3642,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3744,7 +3685,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3774,7 +3714,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3916,7 +3855,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -3949,7 +3887,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4009,7 +3946,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4053,7 +3989,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4083,7 +4018,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4127,7 +4061,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4157,7 +4090,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4302,7 +4234,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4335,7 +4266,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4395,7 +4325,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4439,7 +4368,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4469,7 +4397,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4516,7 +4443,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -4548,7 +4474,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -4595,7 +4520,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -4625,7 +4549,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5043,7 +4966,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5076,7 +4998,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5136,7 +5057,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5180,7 +5100,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5210,7 +5129,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5630,7 +5548,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -5665,7 +5582,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -5727,7 +5643,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5771,7 +5686,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5801,7 +5715,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -5848,7 +5761,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -5880,7 +5792,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
@ -6927,7 +6838,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -6960,7 +6870,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0,
"backgroundColor": "red",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -6993,7 +6902,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0,
"backgroundColor": "red",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -7053,7 +6961,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],

View File

@ -7,7 +7,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -57,7 +56,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -92,7 +90,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -125,7 +122,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -175,7 +171,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],

View File

@ -5,7 +5,6 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -38,7 +37,6 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -71,7 +69,6 @@ exports[`move element > rectangle 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -109,7 +106,6 @@ exports[`move element > rectangles with binding arrow 1`] = `
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -147,7 +143,6 @@ exports[`move element > rectangles with binding arrow 2`] = `
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -180,7 +175,6 @@ exports[`move element > rectangles with binding arrow 3`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": {
"elementId": "id1",

View File

@ -5,7 +5,6 @@ exports[`multi point mode in linear elements > arrow 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -60,7 +59,6 @@ exports[`multi point mode in linear elements > line 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ exports[`select single element on the scene > arrow 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -53,7 +52,6 @@ exports[`select single element on the scene > arrow escape 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -101,7 +99,6 @@ exports[`select single element on the scene > diamond 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -134,7 +131,6 @@ exports[`select single element on the scene > ellipse 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -167,7 +163,6 @@ exports[`select single element on the scene > rectangle 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],

View File

@ -5,7 +5,6 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [],
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -53,7 +52,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0,
"backgroundColor": "blue",
"boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [
@ -90,7 +88,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0,
"backgroundColor": "blue",
"boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [
@ -127,7 +124,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0,
"backgroundColor": "blue",
"boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": [
@ -164,7 +160,6 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
@ -201,7 +196,6 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [],
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -249,7 +243,6 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [],
"customData": undefined,
"endArrowhead": null,
"endBinding": null,
"fillStyle": "solid",
@ -299,7 +292,6 @@ exports[`restoreElements > should restore text element correctly passing value f
"baseline": 0,
"boundElements": [],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 14,
@ -341,7 +333,6 @@ exports[`restoreElements > should restore text element correctly with unknown fo
"baseline": 0,
"boundElements": [],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 1,
"fontSize": 10,

View File

@ -471,7 +471,7 @@ export type ExportOpts = {
// truthiness value will determine whether the action is rendered or not
// (see manager renderAction). We also override canvasAction values in
// excalidraw package index.tsx.
export type CanvasActions = Partial<{
type CanvasActions = Partial<{
changeViewBackgroundColor: boolean;
clearCanvas: boolean;
export: false | ExportOpts;
@ -481,12 +481,9 @@ export type CanvasActions = Partial<{
saveAsImage: boolean;
}>;
export type UIOptions = Partial<{
type UIOptions = Partial<{
dockedSidebarBreakpoint: number;
canvasActions: CanvasActions;
tools: {
image: boolean;
};
/** @deprecated does nothing. Will be removed in 0.15 */
welcomeScreen?: boolean;
}>;