mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-24 16:34:24 +02:00
Compare commits
7 Commits
dwelle/bum
...
feat-add-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48c1f93e3e | ||
|
|
3dd446dc15 | ||
|
|
56c21529db | ||
|
|
a13aed92f2 | ||
|
|
134df7bfbb | ||
|
|
5191cdbe26 | ||
|
|
27fd150a20 |
@@ -1,43 +1,7 @@
|
||||
{
|
||||
"extends": ["plugin:react-hooks/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"root": true,
|
||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||
"rules": {
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"curly": "warn",
|
||||
"dot-notation": "warn",
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["warn", "error", "info"]
|
||||
}
|
||||
],
|
||||
"no-else-return": "warn",
|
||||
"no-lonely-if": "warn",
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
"message": "Use 't(...)' instead of literal text in JSX",
|
||||
"selector": "JSXText[value=/\\w/]"
|
||||
}
|
||||
],
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "warn",
|
||||
"no-var": "warn",
|
||||
"object-shorthand": "warn",
|
||||
"one-var": ["warn", "never"],
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": [
|
||||
"warn",
|
||||
{
|
||||
"destructuring": "all"
|
||||
}
|
||||
],
|
||||
"prefer-template": "warn"
|
||||
"no-restricted-globals": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ const { CLIEngine } = require("eslint");
|
||||
const cli = new CLIEngine({});
|
||||
|
||||
module.exports = {
|
||||
"*.{js,ts,tsx}": (files) => {
|
||||
"*.{js,ts,tsx}": files => {
|
||||
return (
|
||||
"eslint --max-warnings=0 --fix " +
|
||||
files.filter((file) => !cli.isPathIgnored(file)).join(" ")
|
||||
"eslint --max-warnings=0 --fix " + files.filter(file => !cli.isPathIgnored(file)).join(" ")
|
||||
);
|
||||
},
|
||||
"*.{css,scss,json,md,html,yml}": ["prettier --write"],
|
||||
|
||||
@@ -9,7 +9,7 @@ You will need to import the `Footer` component from the package and wrap your co
|
||||
```jsx live
|
||||
function App() {
|
||||
return (
|
||||
<div style={{ height: "500px" }}>
|
||||
<div style={{ height: "500px"}}>
|
||||
<Excalidraw>
|
||||
<Footer>
|
||||
<button
|
||||
@@ -39,7 +39,7 @@ const MobileFooter = ({}) => {
|
||||
<Footer>
|
||||
<button
|
||||
className="custom-footer"
|
||||
style={{ marginLeft: "20px", height: "2rem" }}
|
||||
style= {{ marginLeft: '20px', height: '2rem'}}
|
||||
onClick={() => alert("This is custom footer in mobile menu")}
|
||||
>
|
||||
custom footer
|
||||
@@ -65,4 +65,4 @@ const App = () => (
|
||||
// Need to render when code is span across multiple components
|
||||
// in Live Code blocks editor
|
||||
render(<App />);
|
||||
```
|
||||
```
|
||||
@@ -1,13 +1,7 @@
|
||||
# initialData
|
||||
|
||||
<pre>
|
||||
{ elements?:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>
|
||||
, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
AppState
|
||||
</a> }
|
||||
{ elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> }
|
||||
</pre>
|
||||
|
||||
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
|
||||
@@ -52,7 +46,7 @@ function App() {
|
||||
},
|
||||
],
|
||||
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
|
||||
scrollToContent: true,
|
||||
scrollToContent: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
# Props
|
||||
|
||||
All `props` are _optional_.
|
||||
All `props` are *optional*.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||
| [`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. |
|
||||
| [`onPointerDown`](#onpointerdown) | `function` | \_ | This prop if passed gets triggered on pointer down evenets |
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when the something is pasted in to the scene |
|
||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||
| --- | --- | --- | --- |
|
||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | 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. |
|
||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
|
||||
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
|
||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | \_ | Render function that can be used to render custom stats on the stats dialog. |
|
||||
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | \_ | Render function that renders custom sidebar. |
|
||||
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | \_ | This indicates if the app is in `view` mode. |
|
||||
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | \_ | This indicates if the `zen` mode is enabled |
|
||||
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | \_ | This indicates if the `grid` mode is enabled |
|
||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | \_ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
|
||||
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
|
||||
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | _ | Render function that renders custom sidebar. |
|
||||
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
|
||||
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
|
||||
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |
|
||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component |
|
||||
| [`name`](#name) | `string` | | Name of the drawing |
|
||||
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
|
||||
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
|
||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
|
||||
@@ -95,14 +95,7 @@ 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 +111,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 | null) => boolean
|
||||
(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">ClipboardData</a>, event: ClipboardEvent | 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 +137,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<{ nativeEvent: MouseEvent }>) => void
|
||||
(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>,
|
||||
event: CustomEvent<{ nativeEvent: MouseEvent }>) => void
|
||||
</pre>
|
||||
|
||||
Example:
|
||||
@@ -195,30 +181,31 @@ 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
|
||||
|
||||
@@ -239,7 +226,7 @@ This prop indicates whether to `focus` the Excalidraw component on page load. De
|
||||
Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used.
|
||||
|
||||
```tsx
|
||||
(file: File) => string | Promise<string>;
|
||||
(file: File) => string | Promise<string>
|
||||
```
|
||||
|
||||
### validateEmbeddable
|
||||
@@ -250,4 +237,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.
|
||||
@@ -6,7 +6,8 @@
|
||||
(isMobile: boolean, appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
AppState
|
||||
</a>) => JSX | null
|
||||
</a>
|
||||
) => JSX | null
|
||||
</pre>
|
||||
|
||||
A function returning `JSX` to render `custom` UI in the top right corner of the app.
|
||||
@@ -73,7 +74,6 @@ You can render `custom sidebar` using this prop. This sidebar is the same that t
|
||||
You need to import the `Sidebar` component from `excalidraw` package and pass your content as its `children`. The function `renderSidebar` should return the `Sidebar` instance.
|
||||
|
||||
### Sidebar
|
||||
|
||||
The `<Sidebar>` component takes these props (all are optional except `children`):
|
||||
|
||||
| Prop | Type | Description |
|
||||
@@ -84,7 +84,9 @@ The `<Sidebar>` component takes these props (all are optional except `children`)
|
||||
| `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. |
|
||||
| `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. |
|
||||
|
||||
The sidebar will always include a header with `close / dock` buttons (when applicable). You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
|
||||
The sidebar will always include a header with `close / dock` buttons (when applicable).
|
||||
You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
|
||||
|
||||
|
||||
### Sidebar.Header
|
||||
|
||||
@@ -100,10 +102,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<div style={{ height: "500px" }}>
|
||||
<button
|
||||
className="custom-button"
|
||||
onClick={() => excalidrawAPI.toggleMenu("customSidebar")}
|
||||
>
|
||||
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
|
||||
Toggle Custom Sidebar
|
||||
</button>
|
||||
<Excalidraw
|
||||
@@ -126,11 +125,7 @@ function App() {
|
||||
## renderEmbeddable
|
||||
|
||||
<pre>
|
||||
(element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
AppState
|
||||
</a>
|
||||
) => JSX.Element | null
|
||||
(element: NonDeleted<ExcalidrawEmbeddableElement>, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>) => JSX.Element | null
|
||||
</pre>
|
||||
|
||||
Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements).
|
||||
|
||||
@@ -65,7 +65,7 @@ If user choses to `dock` the sidebar, it will push the right part of the UI towa
|
||||
function App() {
|
||||
return (
|
||||
<div style={{ height: "500px" }}>
|
||||
<Excalidraw UIOptions={{ dockedSidebarBreakpoint: 200 }} />
|
||||
<Excalidraw UIOptions={{dockedSidebarBreakpoint: 200}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,44 +14,35 @@ We're working on much improved export utilities. Stay tuned!
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToCanvas({
|
||||
<br />
|
||||
elements,
|
||||
<br />
|
||||
appState
|
||||
<br />
|
||||
getDimensions,
|
||||
<br />
|
||||
files,
|
||||
<br />
|
||||
exportPadding?: number;
|
||||
<br />
|
||||
}:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">
|
||||
ExportOpts
|
||||
</a>
|
||||
exportToCanvas({<br/>
|
||||
elements,<br/>
|
||||
appState<br/>
|
||||
getDimensions,<br/>
|
||||
files,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a>
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to be exported to canvas. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. |
|
||||
| [`getDimensions`](#getdimensions) | `function` | \_ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
|
||||
| `maxWidthOrHeight` | `number` | \_ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | \_ | The files added to the scene. |
|
||||
| [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
|
||||
| `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. |
|
||||
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
|
||||
|
||||
|
||||
#### getDimensions
|
||||
|
||||
```tsx
|
||||
(width: number, height: number) => {
|
||||
(width: number, height: number) => {
|
||||
width: number,
|
||||
height: number,
|
||||
scale?: number
|
||||
height: number,
|
||||
scale?: number
|
||||
}
|
||||
```
|
||||
|
||||
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
|
||||
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
|
||||
|
||||
**How to use**
|
||||
|
||||
@@ -66,17 +57,17 @@ function App() {
|
||||
const [canvasUrl, setCanvasUrl] = useState("");
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="custom-button"
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const elements = excalidrawAPI.getSceneElements();
|
||||
if (!elements || !elements.length) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const canvas = await exportToCanvas({
|
||||
elements,
|
||||
@@ -85,9 +76,7 @@ function App() {
|
||||
exportWithDarkMode: false,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
getDimensions: () => {
|
||||
return { width: 350, height: 350 };
|
||||
},
|
||||
getDimensions: () => { return {width: 350, height: 350}}
|
||||
});
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = "30px Virgil";
|
||||
@@ -101,38 +90,31 @@ function App() {
|
||||
<img src={canvasUrl} alt="" />
|
||||
</div>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### exportToBlob
|
||||
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToBlob(
|
||||
<br />
|
||||
opts:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">
|
||||
ExportOpts
|
||||
</a>{" "}
|
||||
& {
|
||||
<br />
|
||||
mimeType?: string,
|
||||
<br />
|
||||
quality?: number,
|
||||
<br />
|
||||
exportPadding?: number;
|
||||
<br />
|
||||
})
|
||||
exportToBlob(<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {<br/>
|
||||
mimeType?: string,<br/>
|
||||
quality?: number,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
})
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `opts` | `object` | \_ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
|
||||
| `opts` | `object` | _ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
|
||||
| `mimeType` | `string` | `image/png` | Indicates the image format. |
|
||||
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
|
||||
| `exportPadding` | `number` | `10` | The padding to be added on canvas. |
|
||||
@@ -150,31 +132,26 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToSvg({
|
||||
<br />
|
||||
elements:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>,<br />
|
||||
appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
{" "}
|
||||
AppState
|
||||
</a>,<br />
|
||||
exportPadding: number,
|
||||
<br />
|
||||
metadata: string,
|
||||
<br />
|
||||
files:
|
||||
exportToSvg({<br/>
|
||||
elements:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>,<br/>
|
||||
appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
|
||||
</a>,<br/>
|
||||
exportPadding: number,<br/>
|
||||
metadata: string,<br/>
|
||||
files:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
|
||||
BinaryFiles
|
||||
</a>,<br />
|
||||
});
|
||||
BinaryFiles
|
||||
</a>,<br/>
|
||||
});
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg ` |
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg `|
|
||||
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene |
|
||||
| exportPadding | number | 10 | The `padding` to be added on canvas |
|
||||
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. |
|
||||
@@ -186,21 +163,12 @@ This function returns a promise which resolves to `svg` of the exported drawing.
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToClipboard(
|
||||
<br />
|
||||
opts:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">
|
||||
ExportOpts
|
||||
</a>{" "}
|
||||
& {
|
||||
<br />
|
||||
mimeType?: string,
|
||||
<br />
|
||||
quality?: number;
|
||||
<br />
|
||||
type: 'png' | 'svg' |'json'
|
||||
<br />
|
||||
})
|
||||
exportToClipboard(<br/>
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & {<br/>
|
||||
mimeType?: string,<br/>
|
||||
quality?: number;<br/>
|
||||
type: 'png' | 'svg' |'json'<br/>
|
||||
})
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
@@ -208,7 +176,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
|
||||
| `opts` | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas). |
|
||||
| `mimeType` | `string` | `image/png` | Indicates the image format, this will be used when exporting as `png`. |
|
||||
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg` / `image/webp` MIME types. This will be used when exporting as `png`. |
|
||||
| `type` | 'png' | 'svg' | 'json' | \_ | This determines the format to which the scene data should be `exported`. |
|
||||
| `type` | 'png' | 'svg' | 'json' | _ | This determines the format to which the scene data should be `exported`. |
|
||||
|
||||
**How to use**
|
||||
|
||||
|
||||
@@ -8,15 +8,7 @@ id: "restore"
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreAppState(appState:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
|
||||
ImportedDataState["appState"]
|
||||
</a>
|
||||
,<br />
|
||||
localAppState: Partial<
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
AppState
|
||||
</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
|
||||
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/> localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
|
||||
</pre>
|
||||
|
||||
**_How to use_**
|
||||
@@ -28,36 +20,26 @@ import { restoreAppState } from "@excalidraw/excalidraw";
|
||||
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value.
|
||||
|
||||
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.
|
||||
Use this as a way to not override user's defaults if you persist them. You can pass `null` / `undefined` if not applicable.
|
||||
Use this as a way to not override user's defaults if you persist them.
|
||||
You can pass `null` / `undefined` if not applicable.
|
||||
|
||||
### restoreElements
|
||||
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreElements( elements:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ImportedDataState["elements"]
|
||||
</a>
|
||||
,<br />
|
||||
localElements:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>{" "}
|
||||
| null | undefined):{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>
|
||||
,<br />
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }
|
||||
<br />)
|
||||
restoreElements(
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
)
|
||||
</pre>
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| ---- | ---- | ---- |
|
||||
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements |
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
|
||||
|
||||
#### localElements
|
||||
|
||||
@@ -65,13 +47,12 @@ When `localElements` are supplied, they are used to ensure that existing restore
|
||||
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
|
||||
|
||||
#### opts
|
||||
|
||||
The extra optional parameter to configure restored elements. It has the following attributes
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| Prop | Type | Description|
|
||||
| --- | --- | ------|
|
||||
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
|
||||
| `repairBindings` | `boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
|
||||
**_How to use_**
|
||||
|
||||
@@ -88,29 +69,13 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restore( data:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
|
||||
ImportedDataState
|
||||
</a>
|
||||
,<br />
|
||||
localAppState: Partial<
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
|
||||
AppState
|
||||
</a>
|
||||
> | null | undefined,
|
||||
<br />
|
||||
localElements:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>{" "}
|
||||
| null | undefined
|
||||
<br />
|
||||
):{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">
|
||||
DataState
|
||||
</a>
|
||||
<br />
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br />)
|
||||
restore(
|
||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
|
||||
)
|
||||
</pre>
|
||||
|
||||
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
|
||||
@@ -128,12 +93,8 @@ This function makes sure elements and state is set to appropriate values and set
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreLibraryItems(libraryItems:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
|
||||
ImportedDataState["libraryItems"]
|
||||
</a>
|
||||
,<br />
|
||||
defaultStatus: "published" | "unpublished")
|
||||
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>
|
||||
defaultStatus: "published" | "unpublished")
|
||||
</pre>
|
||||
|
||||
**_How to use_**
|
||||
|
||||
@@ -38,7 +38,6 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||
--color-primary-light: #dcbec9;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx live
|
||||
function App() {
|
||||
return (
|
||||
|
||||
@@ -6,11 +6,13 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
|
||||
|
||||
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||
|
||||
When _Aggressive Anti-Fingerprinting_ is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||
|
||||
We strongly recommend turning it off. You can follow the steps below on how to do so.
|
||||
|
||||
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button 
|
||||
|
||||
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
|
||||

|
||||
|
||||
<div style={{width:'30rem'}}>
|
||||
|
||||
@@ -28,6 +30,8 @@ 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).
|
||||
|
||||
|
||||
|
||||
## Need help?
|
||||
|
||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||
|
||||
@@ -41,9 +41,7 @@ import { useState, useEffect } from "react";
|
||||
export default function App() {
|
||||
const [Excalidraw, setExcalidraw] = useState(null);
|
||||
useEffect(() => {
|
||||
import("@excalidraw/excalidraw").then((comp) =>
|
||||
setExcalidraw(comp.Excalidraw),
|
||||
);
|
||||
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
|
||||
}, []);
|
||||
return <>{Excalidraw && <Excalidraw />}</>;
|
||||
}
|
||||
@@ -82,7 +80,7 @@ import TabItem from "@theme/TabItem";
|
||||
<TabItem value="html" label="html">
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Excalidraw in browser</title>
|
||||
|
||||
75
dev-docs/docs/codebase/json-schema.mdx
Normal file
75
dev-docs/docs/codebase/json-schema.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
# JSON Schema
|
||||
|
||||
The Excalidraw data format uses plaintext JSON.
|
||||
|
||||
## Excalidraw files
|
||||
|
||||
When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) is using the below format.
|
||||
|
||||
### Attributes
|
||||
|
||||
| Attribute | Description | Value |
|
||||
| --- | --- | --- |
|
||||
| `type` | The type of the Excalidraw schema | `"excalidraw"` |
|
||||
| `version` | The version of the Excalidraw schema | number |
|
||||
| `source` | The source URL of the Excalidraw application | `"https://excalidraw.com"` |
|
||||
| `elements` | An array of objects representing excalidraw elements on canvas | Array containing excalidraw element objects |
|
||||
| `appState` | Additional application state/configuration | Object containing application state properties |
|
||||
| `files` | Data for excalidraw `image` elements | Object containing image data |
|
||||
|
||||
### JSON Schema example
|
||||
|
||||
```json
|
||||
{
|
||||
// schema information
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
|
||||
// elements on canvas
|
||||
"elements": [
|
||||
// example element
|
||||
{
|
||||
"id": "pologsyG-tAraPgiN9xP9b",
|
||||
"type": "rectangle",
|
||||
"x": 928,
|
||||
"y": 319,
|
||||
"width": 134,
|
||||
"height": 90
|
||||
/* ...other element properties */
|
||||
}
|
||||
/* other elements */
|
||||
],
|
||||
|
||||
// editor state (canvas config, preferences, ...)
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
|
||||
// files data for "image" elements, using format `{ [fileId]: fileData }`
|
||||
"files": {
|
||||
// example of an image data object
|
||||
"3cebd7720911620a3938ce77243696149da03861": {
|
||||
"mimeType": "image/png",
|
||||
"id": "3cebd7720911620a3938c.77243626149da03861",
|
||||
"dataURL": "data:image/png;base64,iVBORWOKGgoAAAANSUhEUgA=",
|
||||
"created": 1690295874454,
|
||||
"lastRetrieved": 1690295874454
|
||||
}
|
||||
/* ...other image data objects */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Excalidraw clipboard format
|
||||
|
||||
When copying selected excalidraw elements to clipboard, the JSON schema is similar to `.excalidraw` format, except it differs in attributes.
|
||||
|
||||
### Attributes
|
||||
|
||||
| Attribute | Description | Example Value |
|
||||
| --- | --- | --- |
|
||||
| `type` | The type of the Excalidraw document. | "excalidraw/clipboard" |
|
||||
| `elements` | An array of objects representing excalidraw elements on canvas. | Array containing excalidraw element objects (see example below) |
|
||||
| `files` | Data for excalidraw `image` elements. | Object containing image data |
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||
|
||||
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you. For new contributors we would recommend to start with _Easy_ tasks.
|
||||
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
|
||||
For new contributors we would recommend to start with *Easy* tasks.
|
||||
|
||||
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
|
||||
|
||||
@@ -68,7 +69,10 @@ It's also a good idea to consider if your change should include additional tests
|
||||
|
||||
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
||||
|
||||
:::note Some checks, such as the `lint` and `test`, require approval from the maintainers to run. They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval. :::
|
||||
:::note
|
||||
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
|
||||
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
|
||||
:::
|
||||
|
||||
## Translating
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ const sidebars = {
|
||||
},
|
||||
items: ["introduction/development", "introduction/contributing"],
|
||||
},
|
||||
|
||||
{
|
||||
type: "category",
|
||||
label: "@excalidraw/excalidraw",
|
||||
@@ -92,6 +91,11 @@ const sidebars = {
|
||||
"@excalidraw/excalidraw/development",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Codebase",
|
||||
items: ["codebase/json-schema"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
15
package.json
15
package.json
@@ -32,6 +32,7 @@
|
||||
"canvas-roundrect-polyfill": "0.0.1",
|
||||
"clsx": "1.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
@@ -57,6 +58,7 @@
|
||||
"tunnel-rat": "0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@excalidraw/eslint-config": "1.0.3",
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.4.0",
|
||||
@@ -67,23 +69,20 @@
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||
"@typescript-eslint/parser": "6.5.0",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"@vitest/coverage-v8": "0.33.0",
|
||||
"@vitest/ui": "0.32.2",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "16.0.1",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
"jsdom": "22.1.0",
|
||||
"lint-staged": "12.3.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "3.0.3",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.4.2",
|
||||
@@ -113,7 +112,7 @@
|
||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier . --ignore-path=.eslintignore",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "vite",
|
||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||
|
||||
@@ -29,13 +29,10 @@ export const actionSelectAllElementsInFrame = register({
|
||||
elements,
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: elementsInFrame.reduce(
|
||||
(acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ExcalidrawElement["id"], true>,
|
||||
),
|
||||
selectedElementIds: elementsInFrame.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
||||
@@ -215,11 +215,11 @@ const _clearAppStateForStorage = <
|
||||
exportType: ExportType,
|
||||
) => {
|
||||
type ExportableKeys = {
|
||||
[K in keyof typeof APP_STATE_STORAGE_CONF]: (typeof APP_STATE_STORAGE_CONF)[K][ExportType] extends true
|
||||
[K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true
|
||||
? K
|
||||
: never;
|
||||
}[keyof typeof APP_STATE_STORAGE_CONF];
|
||||
const stateForExport = {} as { [K in ExportableKeys]?: (typeof appState)[K] };
|
||||
const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] };
|
||||
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
|
||||
const propConfig = APP_STATE_STORAGE_CONF[key];
|
||||
if (propConfig?.[exportType]) {
|
||||
|
||||
@@ -6,15 +6,12 @@ const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||
source: R,
|
||||
keys: K,
|
||||
) => {
|
||||
return keys.reduce(
|
||||
(acc, key: K[number]) => {
|
||||
if (key in source) {
|
||||
acc[key] = source[key];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Pick<R, K[number]>,
|
||||
) as Pick<R, K[number]>;
|
||||
return keys.reduce((acc, key: K[number]) => {
|
||||
if (key in source) {
|
||||
acc[key] = source[key];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Pick<R, K[number]>) as Pick<R, K[number]>;
|
||||
};
|
||||
|
||||
export type ColorPickerColor =
|
||||
|
||||
@@ -3080,7 +3080,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
tool:
|
||||
| {
|
||||
type:
|
||||
| (typeof SHAPES)[number]["value"]
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
@@ -4770,13 +4770,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
// we need to duplicate because we'll be updating this state
|
||||
lastCoords: { ...origin },
|
||||
originalElements: this.scene.getNonDeletedElements().reduce(
|
||||
(acc, element) => {
|
||||
originalElements: this.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
},
|
||||
new Map() as PointerDownState["originalElements"],
|
||||
),
|
||||
}, new Map() as PointerDownState["originalElements"]),
|
||||
resize: {
|
||||
handleType: false,
|
||||
isResizing: false,
|
||||
@@ -6502,7 +6501,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
nextElements = updateFrameMembershipOfSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
nextElements,
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
|
||||
@@ -107,8 +107,7 @@
|
||||
|
||||
border-radius: 1rem;
|
||||
background-color: var(--button-color);
|
||||
box-shadow:
|
||||
0 3px 5px -1px rgba(0, 0, 0, 0.28),
|
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.28),
|
||||
0 6px 10px 0 rgba(0, 0, 0, 0.14);
|
||||
|
||||
font-family: Cascadia;
|
||||
|
||||
@@ -70,16 +70,12 @@
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
visibility 0s linear 0s,
|
||||
opacity 0.5s;
|
||||
transition: visibility 0s linear 0s, opacity 0.5s;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition:
|
||||
visibility 0s linear 300ms,
|
||||
opacity 0.5s;
|
||||
transition: visibility 0s linear 300ms, opacity 0.5s;
|
||||
transition-delay: 0.8s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,25 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
||||
canvas.classList.add("excalidraw__canvas", "static");
|
||||
}
|
||||
|
||||
canvas.style.width = `${props.appState.width}px`;
|
||||
canvas.style.height = `${props.appState.height}px`;
|
||||
canvas.width = props.appState.width * props.scale;
|
||||
canvas.height = props.appState.height * props.scale;
|
||||
const widthString = `${props.appState.width}px`;
|
||||
const heightString = `${props.appState.height}px`;
|
||||
if (canvas.style.width !== widthString) {
|
||||
canvas.style.width = widthString;
|
||||
}
|
||||
if (canvas.style.height !== heightString) {
|
||||
canvas.style.height = heightString;
|
||||
}
|
||||
|
||||
const scaledWidth = props.appState.width * props.scale;
|
||||
const scaledHeight = props.appState.height * props.scale;
|
||||
// setting width/height resets the canvas even if dimensions not changed,
|
||||
// which would cause flicker when we skip frame (due to throttling)
|
||||
if (canvas.width !== scaledWidth) {
|
||||
canvas.width = scaledWidth;
|
||||
}
|
||||
if (canvas.height !== scaledHeight) {
|
||||
canvas.height = scaledHeight;
|
||||
}
|
||||
|
||||
renderStaticScene(
|
||||
{
|
||||
|
||||
@@ -391,19 +391,13 @@
|
||||
appearance: none;
|
||||
background-image: var(--dropdown-icon);
|
||||
background-repeat: no-repeat;
|
||||
background-position:
|
||||
right 0.7rem top 50%,
|
||||
0 0;
|
||||
background-position: right 0.7rem top 50%, 0 0;
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
background-position:
|
||||
left 0.7rem top 50%,
|
||||
0 0;
|
||||
background-position: left 0.7rem top 50%, 0 0;
|
||||
}
|
||||
|
||||
background-size:
|
||||
0.65em auto,
|
||||
100%;
|
||||
background-size: 0.65em auto, 100%;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||
|
||||
@@ -273,7 +273,7 @@ export const resizeImageFile = async (
|
||||
file: File,
|
||||
opts: {
|
||||
/** undefined indicates auto */
|
||||
outputType?: (typeof MIME_TYPES)["jpg"];
|
||||
outputType?: typeof MIME_TYPES["jpg"];
|
||||
maxWidthOrHeight: number;
|
||||
},
|
||||
): Promise<File> => {
|
||||
|
||||
@@ -92,7 +92,8 @@ const repairBinding = (binding: PointBinding | null) => {
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
/** @deprecated */
|
||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||
@@ -158,6 +159,9 @@ const restoreElementWithProperties = <
|
||||
locked: element.locked ?? false,
|
||||
};
|
||||
|
||||
if ("subtype" in element) {
|
||||
base.subtype = element.subtype;
|
||||
}
|
||||
if ("customData" in element) {
|
||||
base.customData = element.customData;
|
||||
}
|
||||
|
||||
@@ -51,81 +51,84 @@ export type ValidLinearElement = {
|
||||
textAlign?: TextAlign;
|
||||
verticalAlign?: VerticalAlign;
|
||||
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
end?: (
|
||||
end?:
|
||||
| (
|
||||
| {
|
||||
type: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type?: "text";
|
||||
id: ExcalidrawTextElement["id"];
|
||||
text: string;
|
||||
}
|
||||
| (
|
||||
| {
|
||||
type: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type?: "text";
|
||||
id: ExcalidrawTextElement["id"];
|
||||
text: string;
|
||||
}
|
||||
) &
|
||||
Partial<ExcalidrawTextElement>)
|
||||
) &
|
||||
Partial<ExcalidrawTextElement>)
|
||||
) &
|
||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
start?: (
|
||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
start?:
|
||||
| (
|
||||
| {
|
||||
type: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type?: "text";
|
||||
id: ExcalidrawTextElement["id"];
|
||||
text: string;
|
||||
}
|
||||
| (
|
||||
| {
|
||||
type: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
"image" | "text" | "frame" | "embeddable"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type?: "text";
|
||||
id: ExcalidrawTextElement["id"];
|
||||
text: string;
|
||||
}
|
||||
) &
|
||||
Partial<ExcalidrawTextElement>)
|
||||
) &
|
||||
Partial<ExcalidrawTextElement>)
|
||||
) &
|
||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
} & Partial<ExcalidrawLinearElement>;
|
||||
|
||||
export type ValidContainer = {
|
||||
type: Exclude<ExcalidrawGenericElement["type"], "selection">;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
label?: {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: FontFamilyValues;
|
||||
textAlign?: TextAlign;
|
||||
verticalAlign?: VerticalAlign;
|
||||
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
} & ElementConstructorOpts;
|
||||
export type ValidContainer =
|
||||
| {
|
||||
type: Exclude<ExcalidrawGenericElement["type"], "selection">;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
label?: {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: FontFamilyValues;
|
||||
textAlign?: TextAlign;
|
||||
verticalAlign?: VerticalAlign;
|
||||
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||
} & ElementConstructorOpts;
|
||||
|
||||
export type ExcalidrawElementSkeleton =
|
||||
| Extract<
|
||||
|
||||
@@ -31,7 +31,7 @@ const _ce = ({
|
||||
width: w,
|
||||
height: h,
|
||||
angle: a,
|
||||
}) as ExcalidrawElement;
|
||||
} as ExcalidrawElement);
|
||||
|
||||
describe("getElementAbsoluteCoords", () => {
|
||||
it("test x1 coordinate", () => {
|
||||
|
||||
@@ -404,7 +404,7 @@ export class LinearElementEditor {
|
||||
static getEditorMidPoints = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): (typeof editorMidPointsCache)["points"] => {
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
const boundText = getBoundTextElement(element);
|
||||
|
||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||
@@ -501,7 +501,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
}
|
||||
let index = 0;
|
||||
const midPoints: (typeof editorMidPointsCache)["points"] =
|
||||
const midPoints: typeof editorMidPointsCache["points"] =
|
||||
LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
@@ -609,7 +609,7 @@ export class LinearElementEditor {
|
||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||
linearElementEditor: LinearElementEditor | null;
|
||||
} {
|
||||
const ret: ReturnType<(typeof LinearElementEditor)["handlePointerDown"]> = {
|
||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||
didAddPoint: false,
|
||||
hitElement: null,
|
||||
linearElementEditor: null,
|
||||
|
||||
@@ -767,7 +767,7 @@ export const resizeMultipleElements = (
|
||||
false,
|
||||
);
|
||||
|
||||
const update: (typeof elementsAndUpdates)[0]["update"] = {
|
||||
const update: typeof elementsAndUpdates[0]["update"] = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
|
||||
@@ -70,26 +70,20 @@ export const getElementWithTransformHandleType = (
|
||||
zoom: Zoom,
|
||||
pointerType: PointerType,
|
||||
) => {
|
||||
return elements.reduce(
|
||||
(result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const transformHandleType = resizeTest(
|
||||
element,
|
||||
appState,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
zoom,
|
||||
pointerType,
|
||||
);
|
||||
return transformHandleType ? { element, transformHandleType } : null;
|
||||
},
|
||||
null as {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
transformHandleType: MaybeTransformHandleType;
|
||||
} | null,
|
||||
);
|
||||
return elements.reduce((result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const transformHandleType = resizeTest(
|
||||
element,
|
||||
appState,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
zoom,
|
||||
pointerType,
|
||||
);
|
||||
return transformHandleType ? { element, transformHandleType } : null;
|
||||
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
|
||||
};
|
||||
|
||||
export const getTransformHandleTypeFromCoords = (
|
||||
|
||||
@@ -1509,4 +1509,30 @@ describe("textWysiwyg", () => {
|
||||
expect(text.text).toBe("Excalidraw");
|
||||
});
|
||||
});
|
||||
|
||||
it("should bump the version of labelled arrow when label updated", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
const arrow = UI.createElement("arrow", {
|
||||
width: 300,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
mouse.select(arrow);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
let editor = getTextEditor();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
const { version } = arrow;
|
||||
|
||||
mouse.select(arrow);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello\nworld!");
|
||||
editor.blur();
|
||||
|
||||
expect(arrow.version).toEqual(version + 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ExcalidrawTextContainer,
|
||||
} from "./types";
|
||||
import { AppState } from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getBoundTextElementId,
|
||||
getContainerElement,
|
||||
@@ -541,6 +541,9 @@ export const textWysiwyg = ({
|
||||
id: element.id,
|
||||
}),
|
||||
});
|
||||
} else if (isArrowElement(container)) {
|
||||
// updating an arrow label may change bounds, prevent stale cache:
|
||||
bumpVersion(container);
|
||||
}
|
||||
} else {
|
||||
mutateElement(container, {
|
||||
|
||||
@@ -11,18 +11,18 @@ import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||
export type ChartType = "bar" | "line";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
||||
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
||||
export type FontFamilyValues = (typeof FONT_FAMILY)[FontFamilyKeys];
|
||||
export type Theme = (typeof THEME)[keyof typeof THEME];
|
||||
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
|
||||
export type Theme = typeof THEME[keyof typeof THEME];
|
||||
export type FontString = string & { _brand: "fontString" };
|
||||
export type GroupId = string;
|
||||
export type PointerType = "mouse" | "pen" | "touch";
|
||||
export type StrokeRoundness = "round" | "sharp";
|
||||
export type RoundnessType = ValueOf<typeof ROUNDNESS>;
|
||||
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||
export type TextAlign = (typeof TEXT_ALIGN)[keyof typeof TEXT_ALIGN];
|
||||
export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
|
||||
|
||||
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
||||
export type VerticalAlign = (typeof VERTICAL_ALIGN)[VerticalAlignKeys];
|
||||
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
||||
|
||||
type _ExcalidrawElementBase = Readonly<{
|
||||
id: string;
|
||||
@@ -65,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
subtype?: string;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import {
|
||||
convertToExcalidrawElements,
|
||||
Excalidraw,
|
||||
} from "./packages/excalidraw/index";
|
||||
import { API } from "./tests/helpers/api";
|
||||
import { Pointer } from "./tests/helpers/ui";
|
||||
import { Keyboard, Pointer } from "./tests/helpers/ui";
|
||||
import { render } from "./tests/test-utils";
|
||||
|
||||
const { h } = window;
|
||||
@@ -28,83 +29,301 @@ describe("adding elements to frames", () => {
|
||||
}, []);
|
||||
};
|
||||
|
||||
describe("resizing frame over elements", () => {
|
||||
const testElements = async (
|
||||
containerType: "arrow" | "rectangle",
|
||||
initialOrder: ElementType[],
|
||||
expectedOrder: ElementType[],
|
||||
) => {
|
||||
await render(<Excalidraw />);
|
||||
function resizeFrameOverElement(
|
||||
frame: ExcalidrawElement,
|
||||
element: ExcalidrawElement,
|
||||
) {
|
||||
mouse.clickAt(0, 0);
|
||||
mouse.downAt(frame.x + frame.width, frame.y + frame.height);
|
||||
mouse.moveTo(
|
||||
element.x + element.width + 50,
|
||||
element.y + element.height + 50,
|
||||
);
|
||||
mouse.up();
|
||||
}
|
||||
|
||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||
function dragElementIntoFrame(
|
||||
frame: ExcalidrawElement,
|
||||
element: ExcalidrawElement,
|
||||
) {
|
||||
mouse.clickAt(element.x, element.y);
|
||||
mouse.downAt(element.x + element.width / 2, element.y + element.height / 2);
|
||||
mouse.moveTo(frame.x + frame.width / 2, frame.y + frame.height / 2);
|
||||
mouse.up();
|
||||
}
|
||||
|
||||
h.elements = reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
);
|
||||
function selectElementAndDuplicate(
|
||||
element: ExcalidrawElement,
|
||||
moveTo: [number, number] = [element.x + 25, element.y + 25],
|
||||
) {
|
||||
const [x, y] = [
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
];
|
||||
|
||||
assertOrder(h.elements, initialOrder);
|
||||
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
expect(h.elements[2].frameId).toBe(null);
|
||||
|
||||
const container = h.elements[1];
|
||||
|
||||
mouse.clickAt(0, 0);
|
||||
mouse.downAt(frame.x + frame.width, frame.y + frame.height);
|
||||
mouse.moveTo(
|
||||
container.x + container.width + 100,
|
||||
container.y + container.height + 100,
|
||||
);
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.downAt(x, y);
|
||||
mouse.moveTo(moveTo[0], moveTo[1]);
|
||||
mouse.up();
|
||||
assertOrder(h.elements, expectedOrder);
|
||||
});
|
||||
}
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
};
|
||||
function expectEqualIds(expected: ExcalidrawElement[]) {
|
||||
expect(h.elements.map((x) => x.id)).toEqual(expected.map((x) => x.id));
|
||||
}
|
||||
|
||||
it("resizing over text containers / labelled arrows", async () => {
|
||||
await testElements(
|
||||
let frame: ExcalidrawElement;
|
||||
let rect1: ExcalidrawElement;
|
||||
let rect2: ExcalidrawElement;
|
||||
let rect3: ExcalidrawElement;
|
||||
let rect4: ExcalidrawElement;
|
||||
let text: ExcalidrawElement;
|
||||
let arrow: ExcalidrawElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
|
||||
frame = API.createElement({ id: "id0", type: "frame", x: 0, width: 150 });
|
||||
rect1 = API.createElement({
|
||||
id: "id1",
|
||||
type: "rectangle",
|
||||
x: -1000,
|
||||
});
|
||||
rect2 = API.createElement({
|
||||
id: "id2",
|
||||
type: "rectangle",
|
||||
x: 200,
|
||||
width: 50,
|
||||
});
|
||||
rect3 = API.createElement({
|
||||
id: "id3",
|
||||
type: "rectangle",
|
||||
x: 400,
|
||||
width: 50,
|
||||
});
|
||||
rect4 = API.createElement({
|
||||
id: "id4",
|
||||
type: "rectangle",
|
||||
x: 1000,
|
||||
width: 50,
|
||||
});
|
||||
text = API.createElement({
|
||||
id: "id5",
|
||||
type: "text",
|
||||
x: 100,
|
||||
});
|
||||
arrow = API.createElement({
|
||||
id: "id6",
|
||||
type: "arrow",
|
||||
x: 100,
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
});
|
||||
|
||||
const commonTestCases = async (
|
||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||
) => {
|
||||
describe("when frame is in a layer below", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
h.elements = [frame, rect2, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [frame, rect1, rect2, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect1, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [frame, rect3, rect4, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when frame is in a layer above", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [rect2, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
h.elements = [rect2, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect4, rect1, rect3, rect2, frame]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when frame is in an inner layer", async () => {
|
||||
it("should add elements", async () => {
|
||||
h.elements = [rect2, frame, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect4, rect3, rect2, frame, rect1]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resizingTest = async (
|
||||
containerType: "arrow" | "rectangle",
|
||||
initialOrder: ElementType[],
|
||||
expectedOrder: ElementType[],
|
||||
) => {
|
||||
await render(<Excalidraw />);
|
||||
|
||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||
|
||||
h.elements = reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
);
|
||||
|
||||
assertOrder(h.elements, initialOrder);
|
||||
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
expect(h.elements[2].frameId).toBe(null);
|
||||
|
||||
const container = h.elements[1];
|
||||
|
||||
resizeFrameOverElement(frame, container);
|
||||
assertOrder(h.elements, expectedOrder);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
};
|
||||
|
||||
describe("resizing frame over elements", async () => {
|
||||
await commonTestCases(resizeFrameOverElement);
|
||||
|
||||
it("resizing over text containers and labelled arrows", async () => {
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["frame", "rectangle", "text"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["frame", "text", "rectangle"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["rectangle", "text", "frame"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["text", "rectangle", "frame"],
|
||||
["text", "rectangle", "frame"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["frame", "arrow", "text"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["text", "arrow", "frame"],
|
||||
["text", "arrow", "frame"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["frame", "arrow", "text"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
|
||||
// FIXME failing in tests (it fails to add elements to frame for some
|
||||
@@ -118,11 +337,104 @@ describe("adding elements to frames", () => {
|
||||
// ["arrow", "text", "frame"],
|
||||
// ["arrow", "text", "frame"],
|
||||
// );
|
||||
// await testElements(
|
||||
// "arrow",
|
||||
// ["frame", "text", "arrow"],
|
||||
// ["text", "arrow", "frame"],
|
||||
// );
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer below", async () => {
|
||||
h.elements = [frame, arrow, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer above", async () => {
|
||||
h.elements = [arrow, text, frame];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||
h.elements = [arrow, frame, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resizing frame over elements but downwards", async () => {
|
||||
it("should add elements when frame is in a layer below", async () => {
|
||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in a layer above", async () => {
|
||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in an inner layer", async () => {
|
||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dragging elements into the frame", async () => {
|
||||
await commonTestCases(dragElementIntoFrame);
|
||||
|
||||
it("should drag element inside, duplicate it and keep it in frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
selectElementAndDuplicate(rect2);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2_copy, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should drag element inside, duplicate it and remove it from frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
// move the rect2 outside the frame
|
||||
selectElementAndDuplicate(rect2, [-1000, -1000]);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(null);
|
||||
expectEqualIds([rect2_copy, frame, rect2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
77
src/frame.ts
77
src/frame.ts
@@ -468,14 +468,39 @@ export const addElementsToFrame = (
|
||||
}
|
||||
}
|
||||
|
||||
let nextElements = allElements.slice();
|
||||
const allElementsIndex = allElements.reduce(
|
||||
(acc: Record<string, number>, element, index) => {
|
||||
acc[element.id] = index;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const frameIndex = allElementsIndex[frame.id];
|
||||
// need to be calculated before the mutation below occurs
|
||||
const leftFrameBoundaryIndex = findIndex(
|
||||
allElements,
|
||||
(e) => e.frameId === frame.id,
|
||||
);
|
||||
|
||||
const existingFrameChildren = allElements.filter(
|
||||
(element) => element.frameId === frame.id,
|
||||
);
|
||||
|
||||
const addedFrameChildren_left: ExcalidrawElement[] = [];
|
||||
const addedFrameChildren_right: ExcalidrawElement[] = [];
|
||||
|
||||
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
||||
for (const element of omitGroupsContainingFrames(
|
||||
allElements,
|
||||
_elementsToAdd,
|
||||
)) {
|
||||
if (element.frameId !== frame.id && !isFrameElement(element)) {
|
||||
if (allElementsIndex[element.id] > frameIndex) {
|
||||
addedFrameChildren_right.push(element);
|
||||
} else {
|
||||
addedFrameChildren_left.push(element);
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
@@ -483,28 +508,35 @@ export const addElementsToFrame = (
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
|
||||
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
|
||||
|
||||
if (elementIndex < frameBoundary) {
|
||||
nextElements = [
|
||||
...nextElements.slice(0, elementIndex),
|
||||
...nextElements.slice(elementIndex + 1, frameBoundary),
|
||||
element,
|
||||
...nextElements.slice(frameBoundary),
|
||||
];
|
||||
} else if (elementIndex > frameIndex) {
|
||||
nextElements = [
|
||||
...nextElements.slice(0, frameIndex),
|
||||
element,
|
||||
...nextElements.slice(frameIndex, elementIndex),
|
||||
...nextElements.slice(elementIndex + 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frameElement = allElements[frameIndex];
|
||||
const nextFrameChildren = addedFrameChildren_left
|
||||
.concat(existingFrameChildren)
|
||||
.concat(addedFrameChildren_right);
|
||||
|
||||
const nextFrameChildrenMap = nextFrameChildren.reduce(
|
||||
(acc: Record<string, boolean>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const nextOtherElements_left = allElements
|
||||
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
|
||||
const nextOtherElement_right = allElements
|
||||
.slice(frameIndex + 1)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
|
||||
const nextElements = nextOtherElements_left
|
||||
.concat(nextFrameChildren)
|
||||
.concat([frameElement])
|
||||
.concat(nextOtherElement_right);
|
||||
|
||||
return nextElements;
|
||||
};
|
||||
|
||||
@@ -518,6 +550,7 @@ export const removeElementsFromFrame = (
|
||||
for (const element of elementsToRemove) {
|
||||
if (element.frameId) {
|
||||
_elementsToRemove.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.push(boundTextElement);
|
||||
@@ -566,7 +599,7 @@ export const replaceAllElementsInFrame = (
|
||||
);
|
||||
};
|
||||
|
||||
/** does not mutate elements, but return new ones */
|
||||
/** does not mutate elements, but returns new ones */
|
||||
export const updateFrameMembershipOfSelectedElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
appState: AppState,
|
||||
|
||||
@@ -104,38 +104,35 @@ class History {
|
||||
): DehydratedHistoryEntry =>
|
||||
this.dehydrateHistoryEntry({
|
||||
appState: clearAppStatePropertiesForHistory(appState),
|
||||
elements: elements.reduce(
|
||||
(elements, element) => {
|
||||
elements: elements.reduce((elements, element) => {
|
||||
if (
|
||||
isLinearElement(element) &&
|
||||
appState.multiElement &&
|
||||
appState.multiElement.id === element.id
|
||||
) {
|
||||
// don't store multi-point arrow if still has only one point
|
||||
if (
|
||||
isLinearElement(element) &&
|
||||
appState.multiElement &&
|
||||
appState.multiElement.id === element.id
|
||||
appState.multiElement.id === element.id &&
|
||||
element.points.length < 2
|
||||
) {
|
||||
// don't store multi-point arrow if still has only one point
|
||||
if (
|
||||
appState.multiElement &&
|
||||
appState.multiElement.id === element.id &&
|
||||
element.points.length < 2
|
||||
) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
elements.push({
|
||||
...element,
|
||||
// don't store last point if not committed
|
||||
points:
|
||||
element.lastCommittedPoint !==
|
||||
element.points[element.points.length - 1]
|
||||
? element.points.slice(0, -1)
|
||||
: element.points,
|
||||
});
|
||||
} else {
|
||||
elements.push(element);
|
||||
return elements;
|
||||
}
|
||||
return elements;
|
||||
},
|
||||
[] as Mutable<typeof elements>,
|
||||
),
|
||||
|
||||
elements.push({
|
||||
...element,
|
||||
// don't store last point if not committed
|
||||
points:
|
||||
element.lastCommittedPoint !==
|
||||
element.points[element.points.length - 1]
|
||||
? element.points.slice(0, -1)
|
||||
: element.points,
|
||||
});
|
||||
} else {
|
||||
elements.push(element);
|
||||
}
|
||||
return elements;
|
||||
}, [] as Mutable<typeof elements>),
|
||||
});
|
||||
|
||||
shouldCreateEntry(nextEntry: HistoryEntry): boolean {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@babel/preset-env": "7.18.6",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@size-limit/preset-big-lib": "8.2.6",
|
||||
"@size-limit/preset-big-lib": "9.0.0",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
@@ -63,7 +63,7 @@
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"size-limit": "8.2.4",
|
||||
"size-limit": "9.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.3",
|
||||
"ts-loader": "9.3.1",
|
||||
|
||||
@@ -1112,38 +1112,37 @@
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
|
||||
"@size-limit/file@8.2.6":
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-8.2.6.tgz#0e17045a0fa8009fc787c85e3c09f611316f908c"
|
||||
integrity sha512-B7ayjxiJsbtXdIIWazJkB5gezi5WBMecdHTFPMDhI3NwEML1RVvUjAkrb1mPAAkIpt2LVHPnhdCUHjqDdjugwg==
|
||||
"@size-limit/file@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-9.0.0.tgz#eed5415f5bcc8407979e47ffa49ffaf12d2d2378"
|
||||
integrity sha512-oM2UaH2FRq4q22k+R+P6xCpzET10T94LFdSjb9svVu/vOD7NaB9LGcG6se8TW1BExXiyXO4GEhLsBt3uMKM3qA==
|
||||
dependencies:
|
||||
semver "7.5.3"
|
||||
semver "7.5.4"
|
||||
|
||||
"@size-limit/preset-big-lib@8.2.6":
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-8.2.6.tgz#fbff51e7a03fc36b6b3d9103cbe5b3909e35a83e"
|
||||
integrity sha512-63a+yos0QNMVCfx1OWnxBrdQVTlBVGzW5fDXwpWq/hKfP3B89XXHYGeL2Z2f8IXSVeGkAHXnDcTZyIPRaXffVg==
|
||||
"@size-limit/preset-big-lib@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-9.0.0.tgz#ddcf30e7646b66ecc0f8a1a6498a5eda6d82876d"
|
||||
integrity sha512-wc+VNLXjn0z11s1IWevo8+utP7uZGPVDNNe5cNyMFYHv7/pwJtgsd8w2onEkbK1h8x1oJfWlcqFNKAnvD1Bylw==
|
||||
dependencies:
|
||||
"@size-limit/file" "8.2.6"
|
||||
"@size-limit/time" "8.2.6"
|
||||
"@size-limit/webpack" "8.2.6"
|
||||
size-limit "8.2.6"
|
||||
"@size-limit/file" "9.0.0"
|
||||
"@size-limit/time" "9.0.0"
|
||||
"@size-limit/webpack" "9.0.0"
|
||||
size-limit "9.0.0"
|
||||
|
||||
"@size-limit/time@8.2.6":
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-8.2.6.tgz#5d1912bcfc6437f6f59804737ad0538b25c207ed"
|
||||
integrity sha512-fUEPvz7Uq6+oUQxSYbNlJt3tTgQBl1VY21USi/B7ebdnVKLnUx1JyPI9v7imN6XEkB2VpJtnYgjFeLgNrirzMA==
|
||||
"@size-limit/time@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-9.0.0.tgz#44ba75b3cba30736b133dbb3fd740f894a642c87"
|
||||
integrity sha512-//Yba5fRkYqpBZ6MFtjDTSjCpQonDMqkwofpe0G1hMd/5l/3PZXVLDCAU2BW3nQFqTkpeyytFG6Y3jxUqSddiw==
|
||||
dependencies:
|
||||
estimo "^2.3.6"
|
||||
react "^17.0.2"
|
||||
|
||||
"@size-limit/webpack@8.2.6":
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-8.2.6.tgz#3a3c98293b80f7c5fb6e8499199ae6f94f05b463"
|
||||
integrity sha512-y2sB66m5sJxIjZ8SEAzpWbiw3/+bnQHDHfk9cSbV5ChKklq02AlYg8BS5KxGWmMpdyUo4TzpjSCP9oEudY+hxQ==
|
||||
"@size-limit/webpack@9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490"
|
||||
integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
webpack "^5.88.0"
|
||||
webpack "^5.88.2"
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.2"
|
||||
@@ -1694,9 +1693,9 @@ ansi-styles@^4.1.0:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
@@ -2553,9 +2552,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
version "3.2.12"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
||||
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
|
||||
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
@@ -2672,9 +2671,9 @@ fs.realpath@^1.0.0:
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
@@ -2993,7 +2992,7 @@ is-docker@^2.0.0, is-docker@^2.1.1:
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
@@ -3105,7 +3104,7 @@ klona@^2.0.4, klona@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
|
||||
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
|
||||
|
||||
lilconfig@^2.0.6, lilconfig@^2.1.0:
|
||||
lilconfig@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
|
||||
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
|
||||
@@ -3146,7 +3145,7 @@ lodash@^4.17.20, lodash@^4.17.4:
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0:
|
||||
loose-envify@^1.0.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -3360,11 +3359,6 @@ npm-run-path@^4.0.1:
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
object-inspect@^1.9.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
|
||||
@@ -3519,16 +3513,16 @@ picocolors@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
picomatch@^2.2.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
||||
|
||||
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
||||
@@ -3685,14 +3679,6 @@ raw-body@2.5.1:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
readable-stream@^2.0.1:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
@@ -3927,10 +3913,10 @@ semver@7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||
|
||||
semver@7.5.3:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
|
||||
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
|
||||
semver@7.5.4, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
@@ -3939,13 +3925,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
send@0.18.0:
|
||||
version "0.18.0"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
|
||||
@@ -4054,22 +4033,10 @@ sirv@^1.0.7:
|
||||
mime "^2.3.1"
|
||||
totalist "^1.0.0"
|
||||
|
||||
size-limit@8.2.4:
|
||||
version "8.2.4"
|
||||
resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.4.tgz#0ab0df7cbc89007d544a50b451f5fb4d110694ca"
|
||||
integrity sha512-Un16nSreD1v2CYwSorattiJcHuAWqXvg4TsGgzpjnoByqQwsSfCIEQHuaD14HNStzredR8cdsO9oGH91ibypTA==
|
||||
dependencies:
|
||||
bytes-iec "^3.1.1"
|
||||
chokidar "^3.5.3"
|
||||
globby "^11.1.0"
|
||||
lilconfig "^2.0.6"
|
||||
nanospinner "^1.1.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
size-limit@8.2.6:
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.6.tgz#e41dbc74a4d7fc13be72551b6ef31ea50007d18d"
|
||||
integrity sha512-zpznim/tX/NegjoQuRKgWTF4XiB0cn2qt90uJzxYNTFAqexk4b94DOAkBD3TwhC6c3kw2r0KcnA5upziVMZqDg==
|
||||
size-limit@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-9.0.0.tgz#203c47303462a8351976eb26175acea5f4e80447"
|
||||
integrity sha512-DrA7o2DeRN3s+vwCA9nn7Ck9Y4pn9t0GNUwQRpKqBtBmNkl6LA2s/NlNCdtKHrEkRTeYA1ZQ65mnYveo9rUqgA==
|
||||
dependencies:
|
||||
bytes-iec "^3.1.1"
|
||||
chokidar "^3.5.3"
|
||||
@@ -4556,7 +4523,7 @@ webpack@5.76.0:
|
||||
watchpack "^2.4.0"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
webpack@^5.88.0:
|
||||
webpack@^5.88.2:
|
||||
version "5.88.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
|
||||
integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
|
||||
|
||||
@@ -36,7 +36,7 @@ const hashSelectionOpts = (
|
||||
type _ = Assert<
|
||||
SameType<
|
||||
Required<HashableKeys>,
|
||||
Pick<Required<HashableKeys>, (typeof keys)[number]>
|
||||
Pick<Required<HashableKeys>, typeof keys[number]>
|
||||
>
|
||||
>;
|
||||
|
||||
|
||||
@@ -167,20 +167,20 @@ export const _generateElementShape = (
|
||||
rightX - verticalRadius
|
||||
} ${rightY - horizontalRadius}
|
||||
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
|
||||
rightX - verticalRadius
|
||||
} ${rightY + horizontalRadius}
|
||||
rightX - verticalRadius
|
||||
} ${rightY + horizontalRadius}
|
||||
L ${bottomX + verticalRadius} ${bottomY - horizontalRadius}
|
||||
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
|
||||
bottomX - verticalRadius
|
||||
} ${bottomY - horizontalRadius}
|
||||
bottomX - verticalRadius
|
||||
} ${bottomY - horizontalRadius}
|
||||
L ${leftX + verticalRadius} ${leftY + horizontalRadius}
|
||||
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${
|
||||
leftY - horizontalRadius
|
||||
}
|
||||
leftY - horizontalRadius
|
||||
}
|
||||
L ${topX - verticalRadius} ${topY + horizontalRadius}
|
||||
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
||||
topY + horizontalRadius
|
||||
}`,
|
||||
topY + horizontalRadius
|
||||
}`,
|
||||
generateRoughOptions(element, true),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -167,8 +167,8 @@ export const exportToSvg = async (
|
||||
|
||||
exportingFrameClipPath = `<clipPath id=${exportingFrame.id}>
|
||||
<rect transform="translate(${exportingFrame.x + offsetX} ${
|
||||
exportingFrame.y + offsetY
|
||||
}) rotate(${exportingFrame.angle} ${cx} ${cy})"
|
||||
exportingFrame.y + offsetY
|
||||
}) rotate(${exportingFrame.angle} ${cx} ${cy})"
|
||||
width="${exportingFrame.width}"
|
||||
height="${exportingFrame.height}"
|
||||
>
|
||||
@@ -235,13 +235,10 @@ const getCanvasSize = (
|
||||
if (!isExportingWholeCanvas || onlyExportingSingleFrame) {
|
||||
const frames = elements.filter((element) => element.type === "frame");
|
||||
|
||||
const exportedFrameIds = frames.reduce(
|
||||
(acc, frame) => {
|
||||
acc[frame.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, true>,
|
||||
);
|
||||
const exportedFrameIds = frames.reduce((acc, frame) => {
|
||||
acc[frame.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, true>);
|
||||
|
||||
// elements in a frame do not affect the canvas size if we're not exporting
|
||||
// the whole canvas
|
||||
|
||||
@@ -34,13 +34,10 @@ const { h } = window;
|
||||
export class API {
|
||||
static setSelectedElements = (elements: ExcalidrawElement[]) => {
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce(
|
||||
(acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ExcalidrawElement["id"], true>,
|
||||
),
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
|
||||
export type LastActiveTool =
|
||||
| {
|
||||
type:
|
||||
| (typeof SHAPES)[number]["value"]
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
@@ -196,7 +196,7 @@ export type AppState = {
|
||||
} & (
|
||||
| {
|
||||
type:
|
||||
| (typeof SHAPES)[number]["value"]
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
|
||||
@@ -372,7 +372,7 @@ export const updateActiveTool = (
|
||||
data: (
|
||||
| {
|
||||
type:
|
||||
| (typeof SHAPES)[number]["value"]
|
||||
| typeof SHAPES[number]["value"]
|
||||
| "eraser"
|
||||
| "hand"
|
||||
| "frame"
|
||||
|
||||
@@ -188,14 +188,11 @@ const getTargetElementsMap = <T extends ExcalidrawElement>(
|
||||
elements: readonly T[],
|
||||
indices: number[],
|
||||
) => {
|
||||
return indices.reduce(
|
||||
(acc, index) => {
|
||||
const element = elements[index];
|
||||
acc[element.id] = element;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ExcalidrawElement>,
|
||||
);
|
||||
return indices.reduce((acc, index) => {
|
||||
const element = elements[index];
|
||||
acc[element.id] = element;
|
||||
return acc;
|
||||
}, {} as Record<string, ExcalidrawElement>);
|
||||
};
|
||||
|
||||
const _shiftElements = (
|
||||
|
||||
Reference in New Issue
Block a user