Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| baf3ab7d81 | |||
| ef0fcc1537 | |||
| ec26aeead2 | |||
| 62f5475c4a | |||
| 7225915b82 | |||
| 8eb3191b3f | |||
| 4d6d6cf129 | |||
| 208285b7ba | |||
| 372a4868da | |||
| 05800d8599 | |||
| 1f496d9f64 | |||
| e0221ddf20 | |||
| 1bd86942f3 | |||
| fd9a172da9 | |||
| 1f9847ed98 | |||
| 4e4802b19e | |||
| 23eb08088e | |||
| e8a6053251 | |||
| 456433e8f0 | |||
| 38e3a4e8e1 | |||
| 27a8cda8fd | |||
| dd5053149a | |||
| 40ec02b280 | |||
| b81aa19ff9 | |||
| e4ddd08bb1 | |||
| 795176b256 | |||
| be057bde39 | |||
| 94f4b727bb | |||
| 63698572db | |||
| ab3467973f | |||
| 91fe07d9c5 | |||
| 28cc821047 | |||
| 7dc728a459 | |||
| 12c651af6d | |||
| 9d0cafe10b | |||
| fb24221587 | |||
| ef347cc685 | |||
| 2d3b9e0c66 | |||
| bdb0dd064b | |||
| b17ed4dc29 | |||
| b988f67759 | |||
| 089aaa8792 | |||
| 28261c4b29 | |||
| 3fbed86d3e | |||
| 38b3d90fa6 | |||
| 82b597ab8b | |||
| 4c939cefad | |||
| 8f0d9f5230 | |||
| fcde0ac3de | |||
| b07dfba4b8 | |||
| 1089cdb278 | |||
| 7246a6b17a | |||
| 04a96caf78 | |||
| 14c6ea938a | |||
| 87aba3f619 | |||
| c8d4e8c421 | |||
| 512e506798 | |||
| b4e742bda0 | |||
| 5a3f4fd08f | |||
| 34515f2952 | |||
| 08f430b3ac | |||
| 59e74f94e6 | |||
| ddc393bd9d | |||
| 9e5948ac28 | |||
| f86d0f9102 | |||
| ace031e992 | |||
| 45faf7d58f | |||
| 8c558a0f33 | |||
| 65059cb166 | |||
| 9158e2d989 | |||
| 12da1862a0 | |||
| 67fb3210ab | |||
| 13d69d8cef | |||
| 0f6ad916c0 | |||
| 9ee2bf36cf | |||
| 86f5c2ebcf |
@@ -0,0 +1,3 @@
|
|||||||
|
## 2020-10-13
|
||||||
|
|
||||||
|
- Added ability to embed scene source into exported PNG/SVG files so you can import the scene from them (open via `Load` button or drag & drop). #2219
|
||||||
@@ -25,9 +25,6 @@
|
|||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
|
||||||
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
|
|
||||||
</a>
|
|
||||||
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||||
</a>
|
</a>
|
||||||
@@ -73,7 +70,7 @@ The Excalidraw editor (npm package) supports:
|
|||||||
|
|
||||||
## Excalidraw.com
|
## Excalidraw.com
|
||||||
|
|
||||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features:
|
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||||
|
|
||||||
- 📡 PWA support (works offline).
|
- 📡 PWA support (works offline).
|
||||||
- 🤼 Real-time collaboration.
|
- 🤼 Real-time collaboration.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Open the `Menu` in the below playground and you will see the `custom footer` ren
|
|||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.editor.isMobile) {
|
if (device.isMobile) {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ To render an item, its recommended to use `MainMenu.Item`.
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
||||||
| `selected` | `boolean` | No | `false` | Whether item is active |
|
|
||||||
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
||||||
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
|
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
|
||||||
@@ -71,7 +70,6 @@ function App() {
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
|
||||||
| `selected` | `boolean` | No | `false` | Whether item is active |
|
|
||||||
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
|
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
|
||||||
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ Open the `main menu` in the below example to view the footer.
|
|||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.editor.isMobile) {
|
if (device.isMobile) {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
@@ -335,6 +335,7 @@ The `device` has the following `attributes`
|
|||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) |
|
||||||
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
||||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
||||||
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Customizing Styles
|
# Customizing Styles
|
||||||
|
|
||||||
Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
||||||
|
|
||||||
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
|
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### Does this package support collaboration ?
|
### Does this package support collaboration ?
|
||||||
|
|
||||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
||||||
|
|
||||||
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ We strongly recommend turning it off. You can follow the steps below on how to d
|
|||||||
|
|
||||||
2. Once opened, look for **Aggressively Block Fingerprinting**
|
2. Once opened, look for **Aggressively Block Fingerprinting**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
3. Switch to **Block Fingerprinting**
|
3. Switch to **Block Fingerprinting**
|
||||||
|
|
||||||
|
|||||||
@@ -34,44 +34,19 @@ function App() {
|
|||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||||
|
|
||||||
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||||
|
|
||||||
1. Importing Excalidraw once **client** is rendered.
|
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [Excalidraw, setExcalidraw] = useState(null);
|
const [Excalidraw, setExcalidraw] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("@excalidraw/excalidraw").then((comp) =>
|
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
|
||||||
setExcalidraw(comp.Excalidraw),
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
return <>{Excalidraw && <Excalidraw />}</>;
|
return <>{Excalidraw && <Excalidraw />}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
|
||||||
|
|
||||||
2. Using **Next.js Dynamic** import.
|
|
||||||
|
|
||||||
Since Excalidraw doesn't server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. However one drawback is the `Refs` don't work with dynamic import in Next.js. We are working on overcoming this and have a better API.
|
|
||||||
|
|
||||||
```jsx showLineNumbers
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
const Excalidraw = dynamic(
|
|
||||||
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
export default function App() {
|
|
||||||
return <Excalidraw />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
|
||||||
|
|
||||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||||
|
|
||||||
## Browser
|
## Browser
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Frames
|
|
||||||
|
|
||||||
## Ordering
|
|
||||||
|
|
||||||
Frames should be ordered where frame children come first, followed by the frame element itself:
|
|
||||||
|
|
||||||
```
|
|
||||||
[
|
|
||||||
other_element,
|
|
||||||
frame1_child1,
|
|
||||||
frame1_child2,
|
|
||||||
frame1,
|
|
||||||
other_element,
|
|
||||||
frame2_child1,
|
|
||||||
frame2_child2,
|
|
||||||
frame2,
|
|
||||||
other_element,
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
If not ordered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
|
||||||
@@ -15,7 +15,7 @@ In case you want to pick up something from the roadmap, comment on that issue an
|
|||||||
1. Run `yarn` to install dependencies
|
1. Run `yarn` to install dependencies
|
||||||
1. Create a branch for your PR with `git checkout -b your-branch-name`
|
1. Create a branch for your PR with `git checkout -b your-branch-name`
|
||||||
|
|
||||||
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run:
|
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ const sidebars = {
|
|||||||
},
|
},
|
||||||
items: ["introduction/development", "introduction/contributing"],
|
items: ["introduction/development", "introduction/contributing"],
|
||||||
},
|
},
|
||||||
{
|
{ type: "category", label: "Codebase", items: ["codebase/json-schema"] },
|
||||||
type: "category",
|
|
||||||
label: "Codebase",
|
|
||||||
items: ["codebase/json-schema", "codebase/frames"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "@excalidraw/excalidraw",
|
label: "@excalidraw/excalidraw",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const FeatureList = [
|
|||||||
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Want to build your own app powered by Excalidraw but don't know where to
|
Want to build your own app powered by Excalidraw by don't know where to
|
||||||
start?
|
start?
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
+11
-116
@@ -145,14 +145,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/highlight" "^7.18.6"
|
"@babel/highlight" "^7.18.6"
|
||||||
|
|
||||||
"@babel/code-frame@^7.22.13":
|
|
||||||
version "7.22.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
|
|
||||||
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/highlight" "^7.22.13"
|
|
||||||
chalk "^2.4.2"
|
|
||||||
|
|
||||||
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
|
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
|
||||||
version "7.18.8"
|
version "7.18.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
|
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
|
||||||
@@ -210,16 +202,6 @@
|
|||||||
"@jridgewell/gen-mapping" "^0.3.2"
|
"@jridgewell/gen-mapping" "^0.3.2"
|
||||||
jsesc "^2.5.1"
|
jsesc "^2.5.1"
|
||||||
|
|
||||||
"@babel/generator@^7.23.0":
|
|
||||||
version "7.23.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
|
|
||||||
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.23.0"
|
|
||||||
"@jridgewell/gen-mapping" "^0.3.2"
|
|
||||||
"@jridgewell/trace-mapping" "^0.3.17"
|
|
||||||
jsesc "^2.5.1"
|
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure@^7.18.6":
|
"@babel/helper-annotate-as-pure@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
|
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
|
||||||
@@ -283,11 +265,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
|
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
|
||||||
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
|
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
|
||||||
|
|
||||||
"@babel/helper-environment-visitor@^7.22.20":
|
|
||||||
version "7.22.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
|
|
||||||
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
|
|
||||||
|
|
||||||
"@babel/helper-explode-assignable-expression@^7.18.6":
|
"@babel/helper-explode-assignable-expression@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
|
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
|
||||||
@@ -303,14 +280,6 @@
|
|||||||
"@babel/template" "^7.18.6"
|
"@babel/template" "^7.18.6"
|
||||||
"@babel/types" "^7.18.9"
|
"@babel/types" "^7.18.9"
|
||||||
|
|
||||||
"@babel/helper-function-name@^7.23.0":
|
|
||||||
version "7.23.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
|
|
||||||
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/template" "^7.22.15"
|
|
||||||
"@babel/types" "^7.23.0"
|
|
||||||
|
|
||||||
"@babel/helper-hoist-variables@^7.18.6":
|
"@babel/helper-hoist-variables@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
|
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
|
||||||
@@ -318,13 +287,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/helper-hoist-variables@^7.22.5":
|
|
||||||
version "7.22.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
|
|
||||||
integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.22.5"
|
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions@^7.18.9":
|
"@babel/helper-member-expression-to-functions@^7.18.9":
|
||||||
version "7.18.9"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
|
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
|
||||||
@@ -412,28 +374,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/helper-split-export-declaration@^7.22.6":
|
|
||||||
version "7.22.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
|
|
||||||
integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.22.5"
|
|
||||||
|
|
||||||
"@babel/helper-string-parser@^7.22.5":
|
|
||||||
version "7.22.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
|
|
||||||
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
|
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.18.6":
|
"@babel/helper-validator-identifier@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
|
||||||
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
|
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.22.20":
|
|
||||||
version "7.22.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
|
||||||
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.18.6":
|
"@babel/helper-validator-option@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
|
||||||
@@ -467,25 +412,11 @@
|
|||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/highlight@^7.22.13":
|
|
||||||
version "7.22.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
|
|
||||||
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-validator-identifier" "^7.22.20"
|
|
||||||
chalk "^2.4.2"
|
|
||||||
js-tokens "^4.0.0"
|
|
||||||
|
|
||||||
"@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9":
|
"@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9":
|
||||||
version "7.18.9"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
|
||||||
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
|
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
|
||||||
|
|
||||||
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
|
|
||||||
version "7.23.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
|
|
||||||
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
|
|
||||||
|
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
|
||||||
@@ -1216,28 +1147,19 @@
|
|||||||
"@babel/parser" "^7.18.6"
|
"@babel/parser" "^7.18.6"
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/template@^7.22.15":
|
|
||||||
version "7.22.15"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
|
||||||
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.22.13"
|
|
||||||
"@babel/parser" "^7.22.15"
|
|
||||||
"@babel/types" "^7.22.15"
|
|
||||||
|
|
||||||
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
||||||
version "7.23.2"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
|
||||||
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
|
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.22.13"
|
"@babel/code-frame" "^7.18.6"
|
||||||
"@babel/generator" "^7.23.0"
|
"@babel/generator" "^7.18.9"
|
||||||
"@babel/helper-environment-visitor" "^7.22.20"
|
"@babel/helper-environment-visitor" "^7.18.9"
|
||||||
"@babel/helper-function-name" "^7.23.0"
|
"@babel/helper-function-name" "^7.18.9"
|
||||||
"@babel/helper-hoist-variables" "^7.22.5"
|
"@babel/helper-hoist-variables" "^7.18.6"
|
||||||
"@babel/helper-split-export-declaration" "^7.22.6"
|
"@babel/helper-split-export-declaration" "^7.18.6"
|
||||||
"@babel/parser" "^7.23.0"
|
"@babel/parser" "^7.18.9"
|
||||||
"@babel/types" "^7.23.0"
|
"@babel/types" "^7.18.9"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
@@ -1249,15 +1171,6 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.18.6"
|
"@babel/helper-validator-identifier" "^7.18.6"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
|
|
||||||
version "7.23.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
|
|
||||||
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-string-parser" "^7.22.5"
|
|
||||||
"@babel/helper-validator-identifier" "^7.22.20"
|
|
||||||
to-fast-properties "^2.0.0"
|
|
||||||
|
|
||||||
"@colors/colors@1.5.0":
|
"@colors/colors@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||||
@@ -1757,11 +1670,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||||
|
|
||||||
"@jridgewell/resolve-uri@^3.1.0":
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
|
|
||||||
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
|
|
||||||
|
|
||||||
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
|
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||||
@@ -1780,19 +1688,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec@^1.4.14":
|
|
||||||
version "1.4.15"
|
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
|
||||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
|
||||||
|
|
||||||
"@jridgewell/trace-mapping@^0.3.17":
|
|
||||||
version "0.3.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
|
|
||||||
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
|
|
||||||
dependencies:
|
|
||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
|
||||||
|
|
||||||
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
|
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
|
||||||
version "0.3.14"
|
version "0.3.14"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
appState: Partial<AppState>;
|
appState: Partial<AppState>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
onSuccess: () => void;
|
}> = ({ elements, appState, files, onError }) => {
|
||||||
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
@@ -108,7 +107,6 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
try {
|
try {
|
||||||
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
||||||
await exportToExcalidrawPlus(elements, appState, files);
|
await exportToExcalidrawPlus(elements, appState, files);
|
||||||
onSuccess();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
* - DataState refers to full state of the app: appState, elements, images,
|
* - DataState refers to full state of the app: appState, elements, images,
|
||||||
* though some state is saved separately (collab username, library) for one
|
* though some state is saved separately (collab username, library) for one
|
||||||
* reason or another. We also save different data to different storage
|
* reason or another. We also save different data to different sotrage
|
||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
|
|||||||
type: "MOUSE_LOCATION";
|
type: "MOUSE_LOCATION";
|
||||||
payload: {
|
payload: {
|
||||||
socketId: string;
|
socketId: string;
|
||||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
pointer: { x: number; y: number };
|
||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -131,5 +131,5 @@ export class Debug {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
window.debug = Debug;
|
window.debug = Debug;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { trackEvent } from "../src/analytics";
|
|||||||
import { getDefaultAppState } from "../src/appState";
|
import { getDefaultAppState } from "../src/appState";
|
||||||
import { ErrorDialog } from "../src/components/ErrorDialog";
|
import { ErrorDialog } from "../src/components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
||||||
|
import { useMathSubtype } from "../src/element/subtypes/mathjax";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
@@ -303,6 +304,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
const [excalidrawAPI, excalidrawRefCallback] =
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||||
|
|
||||||
|
useMathSubtype(excalidrawAPI);
|
||||||
|
|
||||||
const [collabAPI] = useAtom(collabAPIAtom);
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
@@ -608,7 +611,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
) => {
|
) => {
|
||||||
if (exportedElements.length === 0) {
|
if (exportedElements.length === 0) {
|
||||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
@@ -624,7 +627,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
throw new Error(errorMessage);
|
setErrorMessage(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -634,7 +637,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
const { width, height } = canvas;
|
const { width, height } = canvas;
|
||||||
console.error(error, { width, height });
|
console.error(error, { width, height });
|
||||||
throw new Error(error.message);
|
setErrorMessage(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,7 +694,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
excalidrawAPI={excalidrawRefCallback}
|
ref={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -714,11 +717,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
|
||||||
excalidrawAPI?.updateScene({
|
|
||||||
appState: { openDialog: null },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ describe("Test MobileMenu", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
// @ts-ignore
|
//@ts-ignore
|
||||||
h.app.refreshViewportBreakpoints();
|
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||||
// @ts-ignore
|
|
||||||
h.app.refreshEditorBreakpoints();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -30,15 +28,11 @@ describe("Test MobileMenu", () => {
|
|||||||
it("should set device correctly", () => {
|
it("should set device correctly", () => {
|
||||||
expect(h.app.device).toMatchInlineSnapshot(`
|
expect(h.app.device).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"editor": {
|
"canDeviceFitSidebar": false,
|
||||||
"canFitSidebar": false,
|
"isLandscape": true,
|
||||||
"isMobile": true,
|
"isMobile": true,
|
||||||
},
|
"isSmScreen": false,
|
||||||
"isTouchScreen": false,
|
"isTouchScreen": false,
|
||||||
"viewport": {
|
|
||||||
"isLandscape": false,
|
|
||||||
"isMobile": true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "../../excalidraw-app/collab/reconciliation";
|
} from "../../excalidraw-app/collab/reconciliation";
|
||||||
import { randomInteger } from "../../src/random";
|
import { randomInteger } from "../../src/random";
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { cloneJSON } from "../../src/utils";
|
|
||||||
|
|
||||||
type Id = string;
|
type Id = string;
|
||||||
type ElementLike = {
|
type ElementLike = {
|
||||||
@@ -94,6 +93,8 @@ const cleanElements = (elements: ReconciledElements) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cloneDeep = (data: any) => JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
const test = <U extends `${string}:${"L" | "R"}`>(
|
const test = <U extends `${string}:${"L" | "R"}`>(
|
||||||
local: (Id | ElementLike)[],
|
local: (Id | ElementLike)[],
|
||||||
remote: (Id | ElementLike)[],
|
remote: (Id | ElementLike)[],
|
||||||
@@ -114,15 +115,15 @@ const test = <U extends `${string}:${"L" | "R"}`>(
|
|||||||
"remote reconciliation",
|
"remote reconciliation",
|
||||||
);
|
);
|
||||||
|
|
||||||
const __local = cleanElements(cloneJSON(_remote) as ReconciledElements);
|
const __local = cleanElements(cloneDeep(_remote));
|
||||||
const __remote = addParents(cleanElements(cloneJSON(remoteReconciled)));
|
const __remote = addParents(cleanElements(cloneDeep(remoteReconciled)));
|
||||||
if (bidirectional) {
|
if (bidirectional) {
|
||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
cleanElements(
|
cleanElements(
|
||||||
reconcileElements(
|
reconcileElements(
|
||||||
cloneJSON(__local),
|
cloneDeep(__local),
|
||||||
cloneJSON(__remote),
|
cloneDeep(__remote),
|
||||||
{} as AppState,
|
{} as AppState,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+10
-6
@@ -20,9 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/laser-pointer": "1.2.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
|
||||||
"@excalidraw/random-username": "1.1.0",
|
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
@@ -33,6 +31,7 @@
|
|||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
|
"copyfiles": "2.4.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
@@ -42,19 +41,23 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "1.13.1",
|
"jotai": "1.13.1",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
|
"mathjax-full": "https://github.com/MathJax/MathJax-src#develop",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
"patch-package": "8.0.0",
|
||||||
"perfect-freehand": "1.2.0",
|
"perfect-freehand": "1.2.0",
|
||||||
"pica": "7.1.1",
|
"pica": "7.1.1",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
"points-on-curve": "1.0.1",
|
"points-on-curve": "0.2.0",
|
||||||
|
"postinstall-postinstall": "2.1.0",
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"roughjs": "4.6.4",
|
"replace-in-file": "7.0.1",
|
||||||
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
@@ -113,6 +116,7 @@
|
|||||||
"fix": "yarn fix:other && yarn fix:code",
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
|
"postinstall": "patch-package && yarn --cwd node_modules/mathjax-full compile-mjs && node scripts/beta-mathjax-import-paths.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
"test:coverage:watch": "vitest --coverage --watch",
|
||||||
"test:ui": "yarn test --ui --coverage.enabled=true",
|
"test:ui": "yarn test --ui",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||||
|
index 3b228bb9..c8bcdea5 100644
|
||||||
|
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||||
|
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/shim.js
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-MathJax = Object.assign(global.MathJax || {}, require("./MathJax.js").MathJax);
|
||||||
|
+window.MathJax = Object.assign(window.MathJax || {}, require("./MathJax.js").MathJax);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Load component-based configuration, if any
|
||||||
|
@@ -13,10 +13,13 @@ MathJax.Ajax.Preloading(
|
||||||
|
"[MathJax]/jax/element/mml/jax.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
-require("./jax/element/mml/jax.js");
|
||||||
|
-require("./jax/input/AsciiMath/config.js");
|
||||||
|
-require("./jax/input/AsciiMath/jax.js");
|
||||||
|
+module.exports.AsciiMath = void 0;
|
||||||
|
+(async () => {
|
||||||
|
+ await import("./jax/element/mml/jax.js");
|
||||||
|
+ await import("./jax/input/AsciiMath/config.js");
|
||||||
|
+ await import("./jax/input/AsciiMath/jax.js");
|
||||||
|
|
||||||
|
-require("./jax/element/MmlNode.js");
|
||||||
|
+ await import("./jax/element/MmlNode.js");
|
||||||
|
|
||||||
|
-module.exports.AsciiMath = MathJax.InputJax.AsciiMath;
|
||||||
|
+ module.exports.AsciiMath = MathJax.InputJax.AsciiMath;
|
||||||
|
+})();
|
||||||
|
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||||
|
index 853b0a0e..1e009028 100644
|
||||||
|
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||||
|
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/MathJax.js
|
||||||
|
@@ -19,7 +19,7 @@ exports.MathJax = MathJax;
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
var CONSTRUCTOR = function () {
|
||||||
|
- return function () {return arguments.callee.Init.call(this,arguments)};
|
||||||
|
+ return function fn() {return fn.Init.call(this,Object.assign(arguments,{call:fn}))};
|
||||||
|
};
|
||||||
|
|
||||||
|
BASE.Object = OBJECT({
|
||||||
|
@@ -40,7 +40,7 @@ exports.MathJax = MathJax;
|
||||||
|
Init: function (args) {
|
||||||
|
var obj = this;
|
||||||
|
if (args.length === 1 && args[0] === PROTO) {return obj}
|
||||||
|
- if (!(obj instanceof args.callee)) {obj = new args.callee(PROTO)}
|
||||||
|
+ if (!(obj instanceof args.call)) {obj = new args.call(PROTO)}
|
||||||
|
return obj.Init.apply(obj,args) || obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ exports.MathJax = MathJax;
|
||||||
|
|
||||||
|
prototype: {
|
||||||
|
Init: function () {},
|
||||||
|
- SUPER: function (fn) {return fn.callee.SUPER},
|
||||||
|
+ SUPER: function (fn) {return fn.SUPER},
|
||||||
|
can: function (method) {return typeof(this[method]) === "function"},
|
||||||
|
has: function (property) {return typeof(this[property]) !== "undefined"},
|
||||||
|
isa: function (obj) {return (obj instanceof Object) && (this instanceof obj)}
|
||||||
|
@@ -177,7 +177,7 @@ exports.MathJax = MathJax;
|
||||||
|
// Create a callback from an associative array
|
||||||
|
//
|
||||||
|
var CALLBACK = function (data) {
|
||||||
|
- var cb = function () {return arguments.callee.execute.apply(arguments.callee,arguments)};
|
||||||
|
+ var cb = function fn() {return fn.execute.apply(fn,arguments)};
|
||||||
|
for (var id in CALLBACK.prototype) {
|
||||||
|
if (CALLBACK.prototype.hasOwnProperty(id)) {
|
||||||
|
if (typeof(data[id]) !== 'undefined') {cb[id] = data[id]}
|
||||||
|
diff --git a/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js b/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||||
|
index 96fb9186..473aca11 100644
|
||||||
|
--- a/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||||
|
+++ b/node_modules/mathjax-full/ts/input/asciimath/legacy/jax/element/mml/jax.js
|
||||||
|
@@ -813,9 +813,9 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
if (!(this.isEmbellished()) || typeof(this.core) === "undefined") {return this}
|
||||||
|
return this.data[this.core].CoreMO();
|
||||||
|
},
|
||||||
|
- toString: function () {
|
||||||
|
+ toString: function fn() {
|
||||||
|
if (this.inferred) {return '[' + this.data.join(',') + ']'}
|
||||||
|
- return this.SUPER(arguments).toString.call(this);
|
||||||
|
+ return this.SUPER(fn).toString.call(this);
|
||||||
|
},
|
||||||
|
setTeXclass: function (prev) {
|
||||||
|
var i, m = this.data.length;
|
||||||
|
@@ -1196,12 +1196,12 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
}
|
||||||
|
},
|
||||||
|
linebreakContainer: true,
|
||||||
|
- Append: function () {
|
||||||
|
+ Append: function fn() {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
|
if (!((arguments[i] instanceof MML.mtr) ||
|
||||||
|
(arguments[i] instanceof MML.mlabeledtr))) {arguments[i] = MML.mtr(arguments[i])}
|
||||||
|
}
|
||||||
|
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||||
|
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||||
|
},
|
||||||
|
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||||
|
});
|
||||||
|
@@ -1221,11 +1221,11 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
mtable: {rowalign: true, columnalign: true, groupalign: true}
|
||||||
|
},
|
||||||
|
linebreakContainer: true,
|
||||||
|
- Append: function () {
|
||||||
|
+ Append: function fn() {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
|
if (!(arguments[i] instanceof MML.mtd)) {arguments[i] = MML.mtd(arguments[i])}
|
||||||
|
}
|
||||||
|
- this.SUPER(arguments).Append.apply(this,arguments);
|
||||||
|
+ this.SUPER(fn).Append.apply(this,arguments);
|
||||||
|
},
|
||||||
|
setTeXclass: MML.mbase.setSeparateTeXclasses
|
||||||
|
});
|
||||||
|
@@ -1420,9 +1420,9 @@ MathJax.ElementJax.mml.Augment({
|
||||||
|
|
||||||
|
MML.xml = MML.mbase.Subclass({
|
||||||
|
type: "xml",
|
||||||
|
- Init: function () {
|
||||||
|
+ Init: function fn() {
|
||||||
|
this.div = document.createElement("div");
|
||||||
|
- return this.SUPER(arguments).Init.apply(this,arguments);
|
||||||
|
+ return this.SUPER(fn).Init.apply(this,arguments);
|
||||||
|
},
|
||||||
|
Append: function () {
|
||||||
|
for (var i = 0, m = arguments.length; i < m; i++) {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"src": "apple-touch-icon.png",
|
"src": "apple-touch-icon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "180x180"
|
"sizes": "256x256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// When building MathJax 4.0-beta from source within the Excalidraw tree, some
|
||||||
|
// import paths don't properly translate from `ts/` to `mjs/`. This makes the
|
||||||
|
// Excalidraw build process parse MathJax TypeScript files. The resulting error
|
||||||
|
// messages do not occur if MathJax was built from source outside the
|
||||||
|
// Excalidraw tree. The following regexp eliminates those error messages.
|
||||||
|
require("replace-in-file").sync({
|
||||||
|
files: "node_modules/mathjax-full/mjs/**/*",
|
||||||
|
from: /mathjax-full\/ts/g,
|
||||||
|
to: "mathjax-full/mjs",
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
import { getFontString } from "../utils";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
@@ -48,10 +47,11 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height, baseline } = measureTextElement(
|
||||||
boundTextElement.originalText,
|
boundTextElement,
|
||||||
getFontString(boundTextElement),
|
{
|
||||||
boundTextElement.lineHeight,
|
text: boundTextElement.originalText,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getNormalizedZoom } from "../scene";
|
|||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
import { Bounds } from "../element/bounds";
|
import { Bounds } from "../element/bounds";
|
||||||
import { setCursor } from "../cursor";
|
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@@ -438,6 +437,5 @@ export const actionToggleHandTool = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) => event.key === KEYS.H,
|
||||||
!event.altKey && !event[KEYS.CTRL_OR_CMD] && event.key === KEYS.H,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,43 +3,33 @@ import { register } from "./register";
|
|||||||
import {
|
import {
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
createPasteEvent,
|
|
||||||
probablySupportsClipboardBlob,
|
probablySupportsClipboardBlob,
|
||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
readSystemClipboard,
|
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { exportCanvas, prepareElementsForExport } from "../data/index";
|
import { exportCanvas } from "../data/index";
|
||||||
import { isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isFirefox } from "../constants";
|
|
||||||
|
|
||||||
export const actionCopy = register({
|
export const actionCopy = register({
|
||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: async (elements, appState, event: ClipboardEvent | null, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const elementsToCopy = app.scene.getSelectedElements({
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
copyToClipboard(elementsToCopy, app.files);
|
||||||
await copyToClipboard(elementsToCopy, app.files, event);
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
errorMessage: error.message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
predicate: (elements, appState, appProps, app) => {
|
||||||
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
|
},
|
||||||
contextItemLabel: "labels.copy",
|
contextItemLabel: "labels.copy",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
@@ -48,55 +38,15 @@ export const actionCopy = register({
|
|||||||
export const actionPaste = register({
|
export const actionPaste = register({
|
||||||
name: "paste",
|
name: "paste",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: async (elements, appState, data, app) => {
|
perform: (elements: any, appStates: any, data, app) => {
|
||||||
let types;
|
app.pasteFromClipboard(null);
|
||||||
try {
|
|
||||||
types = await readSystemClipboard();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name === "AbortError" || error.name === "NotAllowedError") {
|
|
||||||
// user probably aborted the action. Though not 100% sure, it's best
|
|
||||||
// to not annoy them with an error message.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`actionPaste ${error.name}: ${error.message}`);
|
|
||||||
|
|
||||||
if (isFirefox) {
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
errorMessage: t("hints.firefox_clipboard_write"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
errorMessage: t("errors.asyncPasteFailedOnRead"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
app.pasteFromClipboard(createPasteEvent({ types }));
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
errorMessage: t("errors.asyncPasteFailedOnParse"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
predicate: (elements, appState, appProps, app) => {
|
||||||
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
@@ -105,10 +55,13 @@ export const actionPaste = register({
|
|||||||
export const actionCut = register({
|
export const actionCut = register({
|
||||||
name: "cut",
|
name: "cut",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
perform: (elements, appState, data, app) => {
|
||||||
actionCopy.perform(elements, appState, event, app);
|
actionCopy.perform(elements, appState, data, app);
|
||||||
return actionDeleteSelected.perform(elements, appState);
|
return actionDeleteSelected.perform(elements, appState);
|
||||||
},
|
},
|
||||||
|
predicate: (elements, appState, appProps, app) => {
|
||||||
|
return app.device.isMobile && !!navigator.clipboard;
|
||||||
|
},
|
||||||
contextItemLabel: "labels.cut",
|
contextItemLabel: "labels.cut",
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
||||||
});
|
});
|
||||||
@@ -122,23 +75,20 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
selectedElementIds: appState.selectedElementIds,
|
||||||
elements,
|
includeBoundTextElement: true,
|
||||||
appState,
|
includeElementsInFrames: true,
|
||||||
true,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
exportedElements,
|
selectedElements.length
|
||||||
|
? selectedElements
|
||||||
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.files,
|
app.files,
|
||||||
{
|
appState,
|
||||||
...appState,
|
|
||||||
exportingFrame,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
@@ -174,17 +124,16 @@ export const actionCopyAsPng = register({
|
|||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas("clipboard", exportedElements, appState, app.files, {
|
await exportCanvas(
|
||||||
...appState,
|
"clipboard",
|
||||||
exportingFrame,
|
selectedElements.length
|
||||||
});
|
? selectedElements
|
||||||
|
: getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
app.files,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ const deleteSelectedElements = (
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { normalizeElementOrder } from "../element/sortElements";
|
|||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
import {
|
import {
|
||||||
bindElementsToFramesAfterDuplication,
|
bindElementsToFramesAfterDuplication,
|
||||||
getFrameChildren,
|
getFrameElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
excludeElementsInFramesFromSelection,
|
||||||
@@ -155,7 +155,7 @@ const duplicateElements = (
|
|||||||
groupId,
|
groupId,
|
||||||
).flatMap((element) =>
|
).flatMap((element) =>
|
||||||
isFrameElement(element)
|
isFrameElement(element)
|
||||||
? [...getFrameChildren(elements, element.id), element]
|
? [...getFrameElements(elements, element.id), element]
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ const duplicateElements = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isElementAFrame) {
|
if (isElementAFrame) {
|
||||||
const elementsInFrame = getFrameChildren(sortedElements, element.id);
|
const elementsInFrame = getFrameElements(sortedElements, element.id);
|
||||||
|
|
||||||
elementsWithClones.push(
|
elementsWithClones.push(
|
||||||
...markAsProcessed([
|
...markAsProcessed([
|
||||||
|
|||||||
@@ -191,15 +191,7 @@ export const actionSaveFileToDisk = register({
|
|||||||
},
|
},
|
||||||
app.files,
|
app.files,
|
||||||
);
|
);
|
||||||
return {
|
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||||
commitToHistory: false,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
openDialog: null,
|
|
||||||
fileHandle,
|
|
||||||
toast: { message: t("toast.fileSaved") },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -217,7 +209,7 @@ export const actionSaveFileToDisk = register({
|
|||||||
icon={saveAs}
|
icon={saveAs}
|
||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useDevice().editor.isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
hidden={!nativeFileSystemSupported}
|
hidden={!nativeFileSystemSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { isInvisiblySmallElement } from "../element";
|
import { isInvisiblySmallElement } from "../element";
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool, resetCursor } from "../utils";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { done } from "../components/icons";
|
import { done } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { resetCursor } from "../cursor";
|
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
@@ -91,9 +90,7 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInvisiblySmallElement(multiPointElement)) {
|
if (isInvisiblySmallElement(multiPointElement)) {
|
||||||
newElements = newElements.filter(
|
newElements = newElements.slice(0, -1);
|
||||||
(el) => el.id !== multiPointElement.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameChildren } from "../frame";
|
import { getFrameElements } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { updateActiveTool } from "../utils";
|
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||||
@@ -21,7 +20,7 @@ export const actionSelectAllElementsInFrame = register({
|
|||||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
const elementsInFrame = getFrameChildren(
|
const elementsInFrame = getFrameElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
selectedFrame.id,
|
selectedFrame.id,
|
||||||
).filter((element) => !(element.type === "text" && element.containerId));
|
).filter((element) => !(element.type === "text" && element.containerId));
|
||||||
|
|||||||
+13
-15
@@ -17,12 +17,15 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
getFrameElements,
|
|
||||||
groupByFrames,
|
groupByFrames,
|
||||||
removeElementsFromFrame,
|
removeElementsFromFrame,
|
||||||
replaceAllElementsInFrame,
|
replaceAllElementsInFrame,
|
||||||
@@ -187,6 +190,13 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const frames = selectedElements
|
||||||
|
.filter((element) => element.frameId)
|
||||||
|
.map((element) =>
|
||||||
|
app.scene.getElement(element.frameId!),
|
||||||
|
) as ExcalidrawFrameElement[];
|
||||||
|
|
||||||
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
||||||
nextElements = nextElements.map((element) => {
|
nextElements = nextElements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
@@ -211,19 +221,7 @@ export const actionUngroup = register({
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
frames.forEach((frame) => {
|
||||||
|
|
||||||
const selectedElementFrameIds = new Set(
|
|
||||||
selectedElements
|
|
||||||
.filter((element) => element.frameId)
|
|
||||||
.map((element) => element.frameId!),
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetFrames = getFrameElements(elements).filter((frame) =>
|
|
||||||
selectedElementFrameIds.has(frame.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
targetFrames.forEach((frame) => {
|
|
||||||
if (frame) {
|
if (frame) {
|
||||||
nextElements = replaceAllElementsInFrame(
|
nextElements = replaceAllElementsInFrame(
|
||||||
nextElements,
|
nextElements,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
@@ -51,6 +52,23 @@ export const actionToggleEditMenu = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionFullScreen = register({
|
||||||
|
name: "toggleFullScreen",
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: { category: "canvas", predicate: (appState) => !isFullScreen() },
|
||||||
|
perform: () => {
|
||||||
|
if (!isFullScreen()) {
|
||||||
|
allowFullScreen();
|
||||||
|
}
|
||||||
|
if (isFullScreen()) {
|
||||||
|
exitFullScreen();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import { Excalidraw } from "../packages/excalidraw/index";
|
|
||||||
import { queryByTestId } from "@testing-library/react";
|
|
||||||
import { render } from "../tests/test-utils";
|
|
||||||
import { UI } from "../tests/helpers/ui";
|
|
||||||
import { API } from "../tests/helpers/api";
|
|
||||||
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
|
||||||
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
|
||||||
|
|
||||||
const { h } = window;
|
|
||||||
|
|
||||||
describe("element locking", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await render(<Excalidraw />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("properties when tool selected", () => {
|
|
||||||
it("should show active background top picks", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
|
|
||||||
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
|
||||||
|
|
||||||
// just in case we change it in the future
|
|
||||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
currentItemBackgroundColor: color,
|
|
||||||
});
|
|
||||||
const activeColor = queryByTestId(
|
|
||||||
document.body,
|
|
||||||
`color-top-pick-${color}`,
|
|
||||||
);
|
|
||||||
expect(activeColor).toHaveClass("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show fill style when background non-transparent", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
|
|
||||||
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
|
||||||
|
|
||||||
// just in case we change it in the future
|
|
||||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
currentItemBackgroundColor: color,
|
|
||||||
currentItemFillStyle: "hachure",
|
|
||||||
});
|
|
||||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
|
||||||
|
|
||||||
expect(hachureFillButton).toHaveClass("active");
|
|
||||||
h.setState({
|
|
||||||
currentItemFillStyle: "solid",
|
|
||||||
});
|
|
||||||
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
|
||||||
expect(solidFillStyle).toHaveClass("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show fill style when background transparent", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
|
||||||
currentItemFillStyle: "hachure",
|
|
||||||
});
|
|
||||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
|
||||||
|
|
||||||
expect(hachureFillButton).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show horizontal text align for text tool", () => {
|
|
||||||
UI.clickTool("text");
|
|
||||||
|
|
||||||
h.setState({
|
|
||||||
currentItemTextAlign: "right",
|
|
||||||
});
|
|
||||||
|
|
||||||
const centerTextAlign = queryByTestId(document.body, `align-right`);
|
|
||||||
expect(centerTextAlign).toBeChecked();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("properties when elements selected", () => {
|
|
||||||
it("should show active styles when single element selected", () => {
|
|
||||||
const rect = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
backgroundColor: "red",
|
|
||||||
fillStyle: "cross-hatch",
|
|
||||||
});
|
|
||||||
h.elements = [rect];
|
|
||||||
API.setSelectedElements([rect]);
|
|
||||||
|
|
||||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
|
||||||
expect(crossHatchButton).toHaveClass("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show fill style selected element's background is transparent", () => {
|
|
||||||
const rect = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
backgroundColor: COLOR_PALETTE.transparent,
|
|
||||||
fillStyle: "cross-hatch",
|
|
||||||
});
|
|
||||||
h.elements = [rect];
|
|
||||||
API.setSelectedElements([rect]);
|
|
||||||
|
|
||||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
|
||||||
expect(crossHatchButton).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should highlight common stroke width of selected elements", () => {
|
|
||||||
const rect1 = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
strokeWidth: STROKE_WIDTH.thin,
|
|
||||||
});
|
|
||||||
const rect2 = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
strokeWidth: STROKE_WIDTH.thin,
|
|
||||||
});
|
|
||||||
h.elements = [rect1, rect2];
|
|
||||||
API.setSelectedElements([rect1, rect2]);
|
|
||||||
|
|
||||||
const thinStrokeWidthButton = queryByTestId(
|
|
||||||
document.body,
|
|
||||||
`strokeWidth-thin`,
|
|
||||||
);
|
|
||||||
expect(thinStrokeWidthButton).toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not highlight any stroke width button if no common style", () => {
|
|
||||||
const rect1 = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
strokeWidth: STROKE_WIDTH.thin,
|
|
||||||
});
|
|
||||||
const rect2 = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
strokeWidth: STROKE_WIDTH.bold,
|
|
||||||
});
|
|
||||||
h.elements = [rect1, rect2];
|
|
||||||
API.setSelectedElements([rect1, rect2]);
|
|
||||||
|
|
||||||
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, `strokeWidth-thin`),
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, `strokeWidth-bold`),
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, `strokeWidth-extraBold`),
|
|
||||||
).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show properties of different element types when selected", () => {
|
|
||||||
const rect = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
strokeWidth: STROKE_WIDTH.bold,
|
|
||||||
});
|
|
||||||
const text = API.createElement({
|
|
||||||
type: "text",
|
|
||||||
fontFamily: FONT_FAMILY.Cascadia,
|
|
||||||
});
|
|
||||||
h.elements = [rect, text];
|
|
||||||
API.setSelectedElements([rect, text]);
|
|
||||||
|
|
||||||
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
|
||||||
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppState, Primitive } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
STROKE_WIDTH,
|
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
@@ -83,6 +82,7 @@ import { getLanguage, t } from "../i18n";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
|
canChangeRoundness,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
@@ -118,44 +118,25 @@ export const changeProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormValue = function <T extends Primitive>(
|
export const getFormValue = function <T>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
defaultValue: T,
|
||||||
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
|
||||||
): T {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
|
return (
|
||||||
let ret: T | null = null;
|
(editingElement && getAttribute(editingElement)) ??
|
||||||
|
(isSomeElementSelected(nonDeletedElements, appState)
|
||||||
if (editingElement) {
|
? getCommonAttributeOfSelectedElements(
|
||||||
ret = getAttribute(editingElement);
|
nonDeletedElements,
|
||||||
}
|
|
||||||
|
|
||||||
if (!ret) {
|
|
||||||
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
|
||||||
|
|
||||||
if (hasSelection) {
|
|
||||||
ret =
|
|
||||||
getCommonAttributeOfSelectedElements(
|
|
||||||
isRelevantElement === true
|
|
||||||
? nonDeletedElements
|
|
||||||
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
) ??
|
)
|
||||||
(typeof defaultValue === "function"
|
: defaultValue) ??
|
||||||
? defaultValue(true)
|
defaultValue
|
||||||
: defaultValue);
|
);
|
||||||
} else {
|
|
||||||
ret =
|
|
||||||
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
@@ -266,7 +247,6 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
true,
|
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
@@ -309,7 +289,6 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
true,
|
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
@@ -328,7 +307,7 @@ export const actionChangeFillStyle = register({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"element",
|
"element",
|
||||||
"changeFillStyle",
|
"changeFillStyle",
|
||||||
`${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
@@ -359,28 +338,23 @@ export const actionChangeFillStyle = register({
|
|||||||
} (${getShortcutKey("Alt-Click")})`,
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
active: allElementsZigZag ? true : undefined,
|
active: allElementsZigZag ? true : undefined,
|
||||||
testId: `fill-hachure`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cross-hatch",
|
value: "cross-hatch",
|
||||||
text: t("labels.crossHatch"),
|
text: t("labels.crossHatch"),
|
||||||
icon: FillCrossHatchIcon,
|
icon: FillCrossHatchIcon,
|
||||||
testId: `fill-cross-hatch`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.solid"),
|
text: t("labels.solid"),
|
||||||
icon: FillSolidIcon,
|
icon: FillSolidIcon,
|
||||||
testId: `fill-solid`,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.fillStyle,
|
(element) => element.fillStyle,
|
||||||
(element) => element.hasOwnProperty("fillStyle"),
|
appState.currentItemFillStyle,
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection ? null : appState.currentItemFillStyle,
|
|
||||||
)}
|
)}
|
||||||
onClick={(value, event) => {
|
onClick={(value, event) => {
|
||||||
const nextValue =
|
const nextValue =
|
||||||
@@ -419,31 +393,26 @@ export const actionChangeStrokeWidth = register({
|
|||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: STROKE_WIDTH.thin,
|
value: 1,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: StrokeWidthBaseIcon,
|
icon: StrokeWidthBaseIcon,
|
||||||
testId: "strokeWidth-thin",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: STROKE_WIDTH.bold,
|
value: 2,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: StrokeWidthBoldIcon,
|
icon: StrokeWidthBoldIcon,
|
||||||
testId: "strokeWidth-bold",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: STROKE_WIDTH.extraBold,
|
value: 4,
|
||||||
text: t("labels.extraBold"),
|
text: t("labels.extraBold"),
|
||||||
icon: StrokeWidthExtraBoldIcon,
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
testId: "strokeWidth-extraBold",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeWidth,
|
(element) => element.strokeWidth,
|
||||||
(element) => element.hasOwnProperty("strokeWidth"),
|
appState.currentItemStrokeWidth,
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection ? null : appState.currentItemStrokeWidth,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -492,9 +461,7 @@ export const actionChangeSloppiness = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.roughness,
|
(element) => element.roughness,
|
||||||
(element) => element.hasOwnProperty("roughness"),
|
appState.currentItemRoughness,
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection ? null : appState.currentItemRoughness,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -542,9 +509,7 @@ export const actionChangeStrokeStyle = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeStyle,
|
(element) => element.strokeStyle,
|
||||||
(element) => element.hasOwnProperty("strokeStyle"),
|
appState.currentItemStrokeStyle,
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection ? null : appState.currentItemStrokeStyle,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -584,7 +549,6 @@ export const actionChangeOpacity = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.opacity,
|
(element) => element.opacity,
|
||||||
true,
|
|
||||||
appState.currentItemOpacity,
|
appState.currentItemOpacity,
|
||||||
) ?? undefined
|
) ?? undefined
|
||||||
}
|
}
|
||||||
@@ -643,12 +607,7 @@ export const actionChangeFontSize = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection
|
|
||||||
? null
|
|
||||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -733,25 +692,21 @@ export const actionChangeFontFamily = register({
|
|||||||
value: FontFamilyValues;
|
value: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
testId: string;
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Virgil,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: FreedrawIcon,
|
icon: FreedrawIcon,
|
||||||
testId: "font-family-virgil",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Helvetica,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
testId: "font-family-normal",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Cascadia,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: FontFamilyCodeIcon,
|
icon: FontFamilyCodeIcon,
|
||||||
testId: "font-family-code",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -774,12 +729,7 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection
|
|
||||||
? null
|
|
||||||
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -856,10 +806,7 @@ export const actionChangeTextAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
appState.currentItemTextAlign,
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection ? null : appState.currentItemTextAlign,
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -935,9 +882,7 @@ export const actionChangeVerticalAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(element) =>
|
VERTICAL_ALIGN.MIDDLE,
|
||||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
|
||||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -1002,9 +947,9 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(element) => element.hasOwnProperty("roundness"),
|
(canChangeRoundness(appState.activeTool.type) &&
|
||||||
(hasSelection) =>
|
appState.currentItemRoundness) ||
|
||||||
hasSelection ? null : appState.currentItemRoundness,
|
null,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -1098,7 +1043,6 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.startArrowhead
|
? element.startArrowhead
|
||||||
: appState.currentItemStartArrowhead,
|
: appState.currentItemStartArrowhead,
|
||||||
true,
|
|
||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
@@ -1145,7 +1089,6 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.endArrowhead
|
? element.endArrowhead
|
||||||
: appState.currentItemEndArrowhead,
|
: appState.currentItemEndArrowhead,
|
||||||
true,
|
|
||||||
appState.currentItemEndArrowhead,
|
appState.currentItemEndArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ import {
|
|||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
getDefaultRoundnessTypeForElement,
|
getDefaultRoundnessTypeForElement,
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
isArrowElement,
|
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { ExcalidrawTextElement } from "../element/types";
|
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
@@ -101,19 +99,16 @@ export const actionPasteStyles = register({
|
|||||||
|
|
||||||
if (isTextElement(newElement)) {
|
if (isTextElement(newElement)) {
|
||||||
const fontSize =
|
const fontSize =
|
||||||
(elementStylesToCopyFrom as ExcalidrawTextElement).fontSize ||
|
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
DEFAULT_FONT_SIZE;
|
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
(elementStylesToCopyFrom as ExcalidrawTextElement).fontFamily ||
|
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
DEFAULT_FONT_FAMILY;
|
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
textAlign:
|
textAlign:
|
||||||
(elementStylesToCopyFrom as ExcalidrawTextElement).textAlign ||
|
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
DEFAULT_TEXT_ALIGN,
|
|
||||||
lineHeight:
|
lineHeight:
|
||||||
(elementStylesToCopyFrom as ExcalidrawTextElement).lineHeight ||
|
elementStylesToCopyFrom.lineHeight ||
|
||||||
getDefaultLineHeight(fontFamily),
|
getDefaultLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
@@ -128,10 +123,7 @@ export const actionPasteStyles = register({
|
|||||||
redrawTextBoundingBox(newElement, container);
|
redrawTextBoundingBox(newElement, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (newElement.type === "arrow") {
|
||||||
newElement.type === "arrow" &&
|
|
||||||
isArrowElement(elementStylesToCopyFrom)
|
|
||||||
) {
|
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
startArrowhead: elementStylesToCopyFrom.startArrowhead,
|
startArrowhead: elementStylesToCopyFrom.startArrowhead,
|
||||||
endArrowhead: elementStylesToCopyFrom.endArrowhead,
|
endArrowhead: elementStylesToCopyFrom.endArrowhead,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const actionToggleGridMode = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||||
objectsSnapModeEnabled: false,
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { CODES, KEYS } from "../keys";
|
|
||||||
import { register } from "./register";
|
|
||||||
|
|
||||||
export const actionToggleObjectsSnapMode = register({
|
|
||||||
name: "objectsSnapMode",
|
|
||||||
viewMode: true,
|
|
||||||
trackEvent: {
|
|
||||||
category: "canvas",
|
|
||||||
predicate: (appState) => !appState.objectsSnapModeEnabled,
|
|
||||||
},
|
|
||||||
perform(elements, appState) {
|
|
||||||
return {
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
objectsSnapModeEnabled: !this.checked!(appState),
|
|
||||||
gridSize: null,
|
|
||||||
},
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
checked: (appState) => appState.objectsSnapModeEnabled,
|
|
||||||
predicate: (elements, appState, appProps) => {
|
|
||||||
return typeof appProps.objectsSnapModeEnabled === "undefined";
|
|
||||||
},
|
|
||||||
contextItemLabel: "buttons.objectsSnapMode",
|
|
||||||
keyTest: (event) =>
|
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
|
|
||||||
});
|
|
||||||
@@ -44,6 +44,7 @@ export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
|||||||
export {
|
export {
|
||||||
actionToggleCanvasMenu,
|
actionToggleCanvasMenu,
|
||||||
actionToggleEditMenu,
|
actionToggleEditMenu,
|
||||||
|
actionFullScreen,
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
} from "./actionMenu";
|
} from "./actionMenu";
|
||||||
|
|
||||||
@@ -79,7 +80,6 @@ export {
|
|||||||
|
|
||||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||||
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
|
|
||||||
|
|
||||||
export { actionToggleStats } from "./actionToggleStats";
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||||
|
|||||||
+66
-15
@@ -2,10 +2,10 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
|
||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
|
ActionPredicateFn,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
@@ -29,7 +29,7 @@ const trackAction = (
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
action.trackEvent.category,
|
action.trackEvent.category,
|
||||||
action.trackEvent.action || action.name,
|
action.trackEvent.action || action.name,
|
||||||
`${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,8 @@ const trackAction = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ActionManager {
|
export class ActionManager {
|
||||||
actions = {} as Record<ActionName, Action>;
|
actions = {} as Record<Action["name"], Action>;
|
||||||
|
actionPredicates = [] as ActionPredicateFn[];
|
||||||
|
|
||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
@@ -68,6 +69,37 @@ export class ActionManager {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerActionPredicate(predicate: ActionPredicateFn) {
|
||||||
|
if (!this.actionPredicates.includes(predicate)) {
|
||||||
|
this.actionPredicates.push(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterActions(
|
||||||
|
filter: ActionPredicateFn,
|
||||||
|
opts?: {
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
data?: Record<string, any>;
|
||||||
|
},
|
||||||
|
): Action[] {
|
||||||
|
// For testing
|
||||||
|
if (this === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||||
|
const appState = this.getAppState();
|
||||||
|
const data = opts?.data;
|
||||||
|
|
||||||
|
const actions: Action[] = [];
|
||||||
|
for (const key in this.actions) {
|
||||||
|
const action = this.actions[key];
|
||||||
|
if (filter(action, elements, appState, data)) {
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
registerAction(action: Action) {
|
registerAction(action: Action) {
|
||||||
this.actions[action.name] = action;
|
this.actions[action.name] = action;
|
||||||
}
|
}
|
||||||
@@ -84,7 +116,7 @@ export class ActionManager {
|
|||||||
(action) =>
|
(action) =>
|
||||||
(action.name in canvasActions
|
(action.name in canvasActions
|
||||||
? canvasActions[action.name as keyof typeof canvasActions]
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
: true) &&
|
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -119,10 +151,10 @@ export class ActionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction<T extends Action>(
|
executeAction(
|
||||||
action: T,
|
action: Action,
|
||||||
source: ActionSource = "api",
|
source: ActionSource = "api",
|
||||||
value: Parameters<T["perform"]>[2] = null,
|
value: any = null,
|
||||||
) {
|
) {
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
@@ -135,7 +167,7 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @param data additional data sent to the PanelComponent
|
||||||
*/
|
*/
|
||||||
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -143,7 +175,7 @@ export class ActionManager {
|
|||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
(name in canvasActions
|
(name in canvasActions
|
||||||
? canvasActions[name as keyof typeof canvasActions]
|
? canvasActions[name as keyof typeof canvasActions]
|
||||||
: true)
|
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@@ -165,6 +197,7 @@ export class ActionManager {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
|
key={name}
|
||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
@@ -178,13 +211,31 @@ export class ActionManager {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (action: Action) => {
|
isActionEnabled = (
|
||||||
const elements = this.getElementsIncludingDeleted();
|
action: Action,
|
||||||
|
opts?: {
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
data?: Record<string, any>;
|
||||||
|
noPredicates?: boolean;
|
||||||
|
},
|
||||||
|
): boolean => {
|
||||||
|
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
|
const data = opts?.data;
|
||||||
|
|
||||||
return (
|
if (
|
||||||
!action.predicate ||
|
!opts?.noPredicates &&
|
||||||
action.predicate(elements, appState, this.app.props, this.app)
|
action.predicate &&
|
||||||
);
|
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let enabled = true;
|
||||||
|
this.actionPredicates.forEach((fn) => {
|
||||||
|
if (!fn(action, elements, appState, data)) {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export type ShortcutName =
|
|||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "objectsSnapMode"
|
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
@@ -75,7 +74,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
objectsSnapMode: [getShortcutKey("Alt+S")],
|
|
||||||
stats: [getShortcutKey("Alt+/")],
|
stats: [getShortcutKey("Alt+/")],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
@@ -85,8 +83,23 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export type CustomShortcutName = string;
|
||||||
const shortcuts = shortcutMap[name];
|
|
||||||
|
let customShortcutMap: Record<CustomShortcutName, string[]> = {};
|
||||||
|
|
||||||
|
export const registerCustomShortcuts = (
|
||||||
|
shortcuts: Record<CustomShortcutName, string[]>,
|
||||||
|
) => {
|
||||||
|
customShortcutMap = { ...customShortcutMap, ...shortcuts };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShortcutFromShortcutName = (
|
||||||
|
name: ShortcutName | CustomShortcutName,
|
||||||
|
) => {
|
||||||
|
const shortcuts =
|
||||||
|
name in customShortcutMap
|
||||||
|
? customShortcutMap[name as CustomShortcutName]
|
||||||
|
: shortcutMap[name as ShortcutName];
|
||||||
// if multiple shortcuts available, take the first one
|
// if multiple shortcuts available, take the first one
|
||||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-2
@@ -32,6 +32,15 @@ type ActionFn = (
|
|||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
|
// Return `true` *unless* `Action` should be disabled
|
||||||
|
// given `elements`, `appState`, and optionally `data`.
|
||||||
|
export type ActionPredicateFn = (
|
||||||
|
action: Action,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
) => boolean;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
export type ActionFilterFn = (action: Action) => void;
|
export type ActionFilterFn = (action: Action) => void;
|
||||||
|
|
||||||
@@ -51,7 +60,6 @@ export type ActionName =
|
|||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "objectsSnapMode"
|
|
||||||
| "stats"
|
| "stats"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
@@ -136,7 +144,7 @@ export type PanelComponentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: string;
|
||||||
PanelComponent?: React.FC<PanelComponentProps>;
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
@@ -158,6 +166,7 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
appProps: ExcalidrawProps,
|
appProps: ExcalidrawProps,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
|
data?: Record<string, any>,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
trackEvent:
|
trackEvent:
|
||||||
|
|||||||
+2
-9
@@ -99,12 +99,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
snapLines: [],
|
|
||||||
originSnapOffset: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
objectsSnapModeEnabled: false,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,6 +146,8 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false, server: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
activeTool: { browser: true, export: false, server: false },
|
activeTool: { browser: true, export: false, server: false },
|
||||||
|
activeSubtypes: { browser: true, export: false, server: false },
|
||||||
|
customData: { browser: true, export: false, server: false },
|
||||||
penMode: { browser: true, export: false, server: false },
|
penMode: { browser: true, export: false, server: false },
|
||||||
penDetected: { browser: true, export: false, server: false },
|
penDetected: { browser: true, export: false, server: false },
|
||||||
errorMessage: { browser: false, export: false, server: false },
|
errorMessage: { browser: false, export: false, server: false },
|
||||||
@@ -212,9 +208,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
pendingImageElementId: { browser: false, export: false, server: false },
|
pendingImageElementId: { browser: false, export: false, server: false },
|
||||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||||
selectedLinearElement: { browser: true, export: false, server: false },
|
selectedLinearElement: { browser: true, export: false, server: false },
|
||||||
snapLines: { browser: false, export: false, server: false },
|
|
||||||
originSnapOffset: { browser: false, export: false, server: false },
|
|
||||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
|||||||
+21
-1
@@ -11,6 +11,8 @@ import {
|
|||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
|
import { AppState } from "./types";
|
||||||
|
import { selectSubtype } from "./element/subtypes";
|
||||||
|
|
||||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
@@ -23,6 +25,8 @@ export interface Spreadsheet {
|
|||||||
title: string | null;
|
title: string | null;
|
||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
values: number[];
|
values: number[];
|
||||||
|
activeSubtypes?: AppState["activeSubtypes"];
|
||||||
|
customData?: AppState["customData"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
|
||||||
@@ -193,13 +197,17 @@ const chartXLabels = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
|
const custom = selectSubtype(spreadsheet, "text");
|
||||||
return (
|
return (
|
||||||
spreadsheet.labels?.map((label, index) => {
|
spreadsheet.labels?.map((label, index) => {
|
||||||
return newTextElement({
|
return newTextElement({
|
||||||
groupIds: [groupId],
|
groupIds: [groupId],
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
...commonProps,
|
...commonProps,
|
||||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
text:
|
||||||
|
label.length > 8 && custom.subtype === undefined
|
||||||
|
? `${label.slice(0, 5)}...`
|
||||||
|
: label,
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
@@ -207,6 +215,7 @@ const chartXLabels = (
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
|
...custom,
|
||||||
});
|
});
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
@@ -227,6 +236,7 @@ const chartYLabels = (
|
|||||||
y: y - BAR_GAP,
|
y: y - BAR_GAP,
|
||||||
text: "0",
|
text: "0",
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxYLabel = newTextElement({
|
const maxYLabel = newTextElement({
|
||||||
@@ -237,6 +247,7 @@ const chartYLabels = (
|
|||||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return [minYLabel, maxYLabel];
|
return [minYLabel, maxYLabel];
|
||||||
@@ -264,6 +275,7 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[chartWidth, 0],
|
[chartWidth, 0],
|
||||||
],
|
],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const yLine = newLinearElement({
|
const yLine = newLinearElement({
|
||||||
@@ -280,6 +292,7 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[0, -chartHeight],
|
[0, -chartHeight],
|
||||||
],
|
],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxLine = newLinearElement({
|
const maxLine = newLinearElement({
|
||||||
@@ -298,6 +311,7 @@ const chartLines = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[chartWidth, 0],
|
[chartWidth, 0],
|
||||||
],
|
],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return [xLine, yLine, maxLine];
|
return [xLine, yLine, maxLine];
|
||||||
@@ -324,6 +338,7 @@ const chartBaseElements = (
|
|||||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
...selectSubtype(spreadsheet, "text"),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -340,6 +355,7 @@ const chartBaseElements = (
|
|||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
|
...selectSubtype(spreadsheet, "rectangle"),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -372,6 +388,7 @@ const chartTypeBar = (
|
|||||||
y: y - barHeight - BAR_GAP,
|
y: y - barHeight - BAR_GAP,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
|
...selectSubtype(spreadsheet, "rectangle"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -424,6 +441,7 @@ const chartTypeLine = (
|
|||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
points: points as any,
|
points: points as any,
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dots = spreadsheet.values.map((value, index) => {
|
const dots = spreadsheet.values.map((value, index) => {
|
||||||
@@ -440,6 +458,7 @@ const chartTypeLine = (
|
|||||||
y: y + cy - BAR_GAP * 2,
|
y: y + cy - BAR_GAP * 2,
|
||||||
width: BAR_GAP,
|
width: BAR_GAP,
|
||||||
height: BAR_GAP,
|
height: BAR_GAP,
|
||||||
|
...selectSubtype(spreadsheet, "ellipse"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -462,6 +481,7 @@ const chartTypeLine = (
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[0, cy],
|
[0, cy],
|
||||||
],
|
],
|
||||||
|
...selectSubtype(spreadsheet, "line"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+19
-188
@@ -1,196 +1,27 @@
|
|||||||
import {
|
import { parseClipboard } from "./clipboard";
|
||||||
createPasteEvent,
|
|
||||||
parseClipboard,
|
|
||||||
serializeAsClipboardJSON,
|
|
||||||
} from "./clipboard";
|
|
||||||
import { API } from "./tests/helpers/api";
|
|
||||||
|
|
||||||
describe("parseClipboard()", () => {
|
describe("Test parseClipboard", () => {
|
||||||
it("should parse JSON as plaintext if not excalidraw-api/clipboard data", async () => {
|
it("should parse valid json correctly", async () => {
|
||||||
let text;
|
let text = "123";
|
||||||
let clipboardData;
|
|
||||||
// -------------------------------------------------------------------------
|
let clipboardData = await parseClipboard({
|
||||||
|
//@ts-ignore
|
||||||
|
clipboardData: {
|
||||||
|
getData: () => text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
text = "123";
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({ types: { "text/plain": text } }),
|
|
||||||
);
|
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
text = "[123]";
|
text = "[123]";
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({ types: { "text/plain": text } }),
|
clipboardData = await parseClipboard({
|
||||||
);
|
//@ts-ignore
|
||||||
|
clipboardData: {
|
||||||
|
getData: () => text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
text = JSON.stringify({ val: 42 });
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({ types: { "text/plain": text } }),
|
|
||||||
);
|
|
||||||
expect(clipboardData.text).toBe(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse valid excalidraw JSON if inside text/plain", async () => {
|
|
||||||
const rect = API.createElement({ type: "rectangle" });
|
|
||||||
|
|
||||||
const json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
|
||||||
const clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/plain": json,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse valid excalidraw JSON if inside text/html", async () => {
|
|
||||||
const rect = API.createElement({ type: "rectangle" });
|
|
||||||
|
|
||||||
let json;
|
|
||||||
let clipboardData;
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": json,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<div> ${json}</div>`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse <image> `src` urls out of text/html", async () => {
|
|
||||||
let clipboardData;
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<img src="https://example.com/image.png" />`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.mixedContent).toEqual([
|
|
||||||
{
|
|
||||||
type: "imageUrl",
|
|
||||||
value: "https://example.com/image.png",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.mixedContent).toEqual([
|
|
||||||
{
|
|
||||||
type: "imageUrl",
|
|
||||||
value: "https://example.com/image.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "imageUrl",
|
|
||||||
value: "https://example.com/image2.png",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse text content alongside <image> `src` urls out of text/html", async () => {
|
|
||||||
const clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.mixedContent).toEqual([
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
// trimmed
|
|
||||||
value: "hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "imageUrl",
|
|
||||||
value: "https://example.com/image.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
value: "my friend!",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse spreadsheet from either text/plain and text/html", async () => {
|
|
||||||
let clipboardData;
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/plain": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
clipboardData = await parseClipboard(
|
|
||||||
createPasteEvent({
|
|
||||||
types: {
|
|
||||||
"text/html": `<html>
|
|
||||||
<body>
|
|
||||||
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
|
||||||
</body>
|
|
||||||
</html>`,
|
|
||||||
"text/plain": `a b
|
|
||||||
1 2
|
|
||||||
4 5
|
|
||||||
7 10`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(clipboardData.spreadsheet).toEqual({
|
|
||||||
title: "b",
|
|
||||||
labels: ["1", "4", "7"],
|
|
||||||
values: [2, 5, 10],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+90
-257
@@ -2,19 +2,15 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { BinaryFiles } from "./types";
|
import { AppState, BinaryFiles } from "./types";
|
||||||
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import {
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
ALLOWED_PASTE_MIME_TYPES,
|
|
||||||
EXPORT_DATA_TYPES,
|
|
||||||
MIME_TYPES,
|
|
||||||
} from "./constants";
|
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { getContainingFrame } from "./frame";
|
import { getContainingFrame } from "./frame";
|
||||||
import { isMemberOf, isPromiseLike } from "./utils";
|
import { isPromiseLike, isTestEnv } from "./utils";
|
||||||
import { t } from "./i18n";
|
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@@ -22,23 +18,17 @@ type ElementsClipboard = {
|
|||||||
files: BinaryFiles | undefined;
|
files: BinaryFiles | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
|
|
||||||
|
|
||||||
export interface ClipboardData {
|
export interface ClipboardData {
|
||||||
spreadsheet?: Spreadsheet;
|
spreadsheet?: Spreadsheet;
|
||||||
elements?: readonly ExcalidrawElement[];
|
elements?: readonly ExcalidrawElement[];
|
||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
mixedContent?: PastedMixedContent;
|
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
programmaticAPI?: boolean;
|
programmaticAPI?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
let CLIPBOARD = "";
|
||||||
|
let PREFER_APP_CLIPBOARD = false;
|
||||||
type ParsedClipboardEvent =
|
|
||||||
| { type: "text"; value: string }
|
|
||||||
| { type: "mixedContent"; value: PastedMixedContent };
|
|
||||||
|
|
||||||
export const probablySupportsClipboardReadText =
|
export const probablySupportsClipboardReadText =
|
||||||
"clipboard" in navigator && "readText" in navigator.clipboard;
|
"clipboard" in navigator && "readText" in navigator.clipboard;
|
||||||
@@ -68,61 +58,10 @@ const clipboardContainsElements = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPasteEvent = ({
|
export const copyToClipboard = async (
|
||||||
types,
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
files,
|
files: BinaryFiles | null,
|
||||||
}: {
|
) => {
|
||||||
types?: { [key in AllowedPasteMimeTypes]?: string };
|
|
||||||
files?: File[];
|
|
||||||
}) => {
|
|
||||||
if (!types && !files) {
|
|
||||||
console.warn("createPasteEvent: no types or files provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = new ClipboardEvent("paste", {
|
|
||||||
clipboardData: new DataTransfer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (types) {
|
|
||||||
for (const [type, value] of Object.entries(types)) {
|
|
||||||
try {
|
|
||||||
event.clipboardData?.setData(type, value);
|
|
||||||
if (event.clipboardData?.getData(type) !== value) {
|
|
||||||
throw new Error(`Failed to set "${type}" as clipboardData item`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files) {
|
|
||||||
let idx = -1;
|
|
||||||
for (const file of files) {
|
|
||||||
idx++;
|
|
||||||
try {
|
|
||||||
event.clipboardData?.items.add(file);
|
|
||||||
if (event.clipboardData?.files[idx] !== file) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to set file "${file.name}" as clipboardData item`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return event;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeAsClipboardJSON = ({
|
|
||||||
elements,
|
|
||||||
files,
|
|
||||||
}: {
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
files: BinaryFiles | null;
|
|
||||||
}) => {
|
|
||||||
const framesToCopy = new Set(
|
const framesToCopy = new Set(
|
||||||
elements.filter((element) => element.type === "frame"),
|
elements.filter((element) => element.type === "frame"),
|
||||||
);
|
);
|
||||||
@@ -144,7 +83,7 @@ export const serializeAsClipboardJSON = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// select bound text elements when copying
|
// select binded text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
@@ -163,20 +102,34 @@ export const serializeAsClipboardJSON = ({
|
|||||||
}),
|
}),
|
||||||
files: files ? _files : undefined,
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
|
const json = JSON.stringify(contents);
|
||||||
|
|
||||||
return JSON.stringify(contents);
|
if (isTestEnv()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLIPBOARD = json;
|
||||||
|
|
||||||
|
try {
|
||||||
|
PREFER_APP_CLIPBOARD = false;
|
||||||
|
await copyTextToSystemClipboard(json);
|
||||||
|
} catch (error: any) {
|
||||||
|
PREFER_APP_CLIPBOARD = true;
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyToClipboard = async (
|
const getAppClipboard = (): Partial<ElementsClipboard> => {
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
if (!CLIPBOARD) {
|
||||||
files: BinaryFiles | null,
|
return {};
|
||||||
/** supply if available to make the operation more certain to succeed */
|
}
|
||||||
clipboardEvent?: ClipboardEvent | null,
|
|
||||||
) => {
|
try {
|
||||||
await copyTextToSystemClipboard(
|
return JSON.parse(CLIPBOARD);
|
||||||
serializeAsClipboardJSON({ elements, files }),
|
} catch (error: any) {
|
||||||
clipboardEvent,
|
console.error(error);
|
||||||
);
|
return {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePotentialSpreadsheet = (
|
const parsePotentialSpreadsheet = (
|
||||||
@@ -189,137 +142,22 @@ const parsePotentialSpreadsheet = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** internal, specific to parsing paste events. Do not reuse. */
|
|
||||||
function parseHTMLTree(el: ChildNode) {
|
|
||||||
let result: PastedMixedContent = [];
|
|
||||||
for (const node of el.childNodes) {
|
|
||||||
if (node.nodeType === 3) {
|
|
||||||
const text = node.textContent?.trim();
|
|
||||||
if (text) {
|
|
||||||
result.push({ type: "text", value: text });
|
|
||||||
}
|
|
||||||
} else if (node instanceof HTMLImageElement) {
|
|
||||||
const url = node.getAttribute("src");
|
|
||||||
if (url && url.startsWith("http")) {
|
|
||||||
result.push({ type: "imageUrl", value: url });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result = result.concat(parseHTMLTree(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybeParseHTMLPaste = (
|
|
||||||
event: ClipboardEvent,
|
|
||||||
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
|
||||||
const html = event.clipboardData?.getData("text/html");
|
|
||||||
|
|
||||||
if (!html) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
||||||
|
|
||||||
const content = parseHTMLTree(doc.body);
|
|
||||||
|
|
||||||
if (content.length) {
|
|
||||||
return { type: "mixedContent", value: content };
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`error in parseHTMLFromPaste: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readSystemClipboard = async () => {
|
|
||||||
const types: { [key in AllowedPasteMimeTypes]?: string } = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard?.readText) {
|
|
||||||
return { "text/plain": await navigator.clipboard?.readText() };
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (navigator.clipboard?.read) {
|
|
||||||
console.warn(
|
|
||||||
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let clipboardItems: ClipboardItems;
|
|
||||||
|
|
||||||
try {
|
|
||||||
clipboardItems = await navigator.clipboard?.read();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name === "DataError") {
|
|
||||||
console.warn(
|
|
||||||
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
|
|
||||||
);
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of clipboardItems) {
|
|
||||||
for (const type of item.types) {
|
|
||||||
if (!isMemberOf(ALLOWED_PASTE_MIME_TYPES, type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
types[type] = await (await item.getType(type)).text();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn(
|
|
||||||
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(types).length === 0) {
|
|
||||||
console.warn("No clipboard data found from clipboard.read().");
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses "paste" ClipboardEvent.
|
* Retrieves content from system clipboard (either from ClipboardEvent or
|
||||||
|
* via async clipboard API if supported)
|
||||||
*/
|
*/
|
||||||
const parseClipboardEvent = async (
|
export const getSystemClipboard = async (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent | null,
|
||||||
isPlainPaste = false,
|
): Promise<string> => {
|
||||||
): Promise<ParsedClipboardEvent> => {
|
|
||||||
try {
|
try {
|
||||||
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
const text = event
|
||||||
|
? event.clipboardData?.getData("text/plain")
|
||||||
|
: probablySupportsClipboardReadText &&
|
||||||
|
(await navigator.clipboard.readText());
|
||||||
|
|
||||||
if (mixedContent) {
|
return (text || "").trim();
|
||||||
if (mixedContent.value.every((item) => item.type === "text")) {
|
|
||||||
return {
|
|
||||||
type: "text",
|
|
||||||
value:
|
|
||||||
event.clipboardData?.getData("text/plain") ||
|
|
||||||
mixedContent.value
|
|
||||||
.map((item) => item.value)
|
|
||||||
.join("\n")
|
|
||||||
.trim(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return mixedContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = event.clipboardData?.getData("text/plain");
|
|
||||||
|
|
||||||
return { type: "text", value: (text || "").trim() };
|
|
||||||
} catch {
|
} catch {
|
||||||
return { type: "text", value: "" };
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -327,32 +165,39 @@ const parseClipboardEvent = async (
|
|||||||
* Attempts to parse clipboard. Prefers system clipboard.
|
* Attempts to parse clipboard. Prefers system clipboard.
|
||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent,
|
event: ClipboardEvent | null,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
|
appState?: AppState,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
const systemClipboard = await getSystemClipboard(event);
|
||||||
|
|
||||||
if (parsedEventData.type === "mixedContent") {
|
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||||
return {
|
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
||||||
mixedContent: parsedEventData.value,
|
// elements
|
||||||
};
|
if (
|
||||||
|
!systemClipboard ||
|
||||||
|
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
|
||||||
|
) {
|
||||||
|
return getAppClipboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// if system clipboard contains spreadsheet, use it even though it's
|
||||||
// if system clipboard contains spreadsheet, use it even though it's
|
// technically possible it's staler than in-app clipboard
|
||||||
// technically possible it's staler than in-app clipboard
|
const spreadsheetResult =
|
||||||
const spreadsheetResult =
|
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
|
||||||
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
if (spreadsheetResult) {
|
||||||
return spreadsheetResult;
|
if ("spreadsheet" in spreadsheetResult) {
|
||||||
|
spreadsheetResult.spreadsheet.activeSubtypes = appState?.activeSubtypes;
|
||||||
|
spreadsheetResult.spreadsheet.customData = appState?.customData;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
return spreadsheetResult;
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appClipboardData = getAppClipboard();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(parsedEventData.value);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
const programmaticAPI =
|
const programmaticAPI =
|
||||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
@@ -365,9 +210,18 @@ export const parseClipboard = async (
|
|||||||
programmaticAPI,
|
programmaticAPI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (e) {}
|
||||||
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
return { text: parsedEventData.value };
|
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||||
|
// support storing to system clipboard on copy
|
||||||
|
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||||
|
? {
|
||||||
|
...appClipboardData,
|
||||||
|
text: isPlainPaste
|
||||||
|
? JSON.stringify(appClipboardData.elements, null, 2)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: { text: systemClipboard };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
@@ -400,49 +254,28 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (
|
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||||
text: string | null,
|
let copied = false;
|
||||||
clipboardEvent?: ClipboardEvent | null,
|
|
||||||
) => {
|
|
||||||
// (1) first try using Async Clipboard API
|
|
||||||
if (probablySupportsClipboardWriteText) {
|
if (probablySupportsClipboardWriteText) {
|
||||||
try {
|
try {
|
||||||
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
||||||
// not focused
|
// not focused
|
||||||
await navigator.clipboard.writeText(text || "");
|
await navigator.clipboard.writeText(text || "");
|
||||||
return;
|
copied = true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
// Note that execCommand doesn't allow copying empty strings, so if we're
|
||||||
try {
|
// clearing clipboard using this API, we must copy at least an empty char
|
||||||
if (clipboardEvent) {
|
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
||||||
clipboardEvent.clipboardData?.setData("text/plain", text || "");
|
throw new Error("couldn't copy");
|
||||||
if (clipboardEvent.clipboardData?.getData("text/plain") !== text) {
|
|
||||||
throw new Error("Failed to setData on clipboardEvent");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// (3) if that fails, use document.execCommand
|
|
||||||
if (!copyTextViaExecCommand(text)) {
|
|
||||||
throw new Error(t("errors.copyToSystemClipboardFailed"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
||||||
const copyTextViaExecCommand = (text: string | null) => {
|
const copyTextViaExecCommand = (text: string) => {
|
||||||
// execCommand doesn't allow copying empty strings, so if we're
|
|
||||||
// clearing clipboard using this API, we must copy at least an empty char
|
|
||||||
if (!text) {
|
|
||||||
text = " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
.undo-redo-buttons {
|
.undo-redo-buttons {
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-button,
|
.zoom-button,
|
||||||
.undo-redo-buttons button {
|
.undo-redo-buttons button {
|
||||||
|
border: 1px solid var(--default-border-color) !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
background-color: var(--color-surface-low) !important;
|
background-color: transparent !important;
|
||||||
font-size: 0.875rem !important;
|
font-size: 0.875rem !important;
|
||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|||||||
+159
-86
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import {
|
import {
|
||||||
@@ -11,15 +11,22 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
hasStrokeStyle,
|
hasStrokeStyle,
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
import { UIAppState, Zoom } from "../types";
|
||||||
import { capitalizeString, isTransparent } from "../utils";
|
import {
|
||||||
|
capitalizeString,
|
||||||
|
isTransparent,
|
||||||
|
updateActiveTool,
|
||||||
|
setCursorForShape,
|
||||||
|
} from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { SubtypeShapeActions, SubtypeToggles } from "./Subtypes";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
@@ -30,13 +37,7 @@ import {
|
|||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
import {
|
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||||
EmbedIcon,
|
|
||||||
extraToolsIcon,
|
|
||||||
frameToolIcon,
|
|
||||||
mermaidLogoIcon,
|
|
||||||
laserPointerToolIcon,
|
|
||||||
} from "./icons";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
@@ -66,8 +67,7 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
(hasBackground(appState.activeTool.type) &&
|
hasBackground(appState.activeTool.type) ||
|
||||||
!isTransparent(appState.currentItemBackgroundColor)) ||
|
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
@@ -101,6 +101,7 @@ export const SelectedShapeActions = ({
|
|||||||
{showChangeBackgroundIcons && (
|
{showChangeBackgroundIcons && (
|
||||||
<div>{renderAction("changeBackgroundColor")}</div>
|
<div>{renderAction("changeBackgroundColor")}</div>
|
||||||
)}
|
)}
|
||||||
|
<SubtypeShapeActions elements={targetElements} />
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
@@ -124,15 +125,14 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(appState.activeTool.type === "text" ||
|
{(hasText(appState.activeTool.type) ||
|
||||||
targetElements.some(isTextElement)) && (
|
targetElements.some((element) => hasText(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{(appState.activeTool.type === "text" ||
|
{suppportsHorizontalAlign(targetElements) &&
|
||||||
suppportsHorizontalAlign(targetElements)) &&
|
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -202,8 +202,8 @@ export const SelectedShapeActions = ({
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
{!device.isMobile && renderAction("duplicateSelection")}
|
||||||
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
{!device.isMobile && renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{showLinkIcon && renderAction("hyperlink")}
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
@@ -215,20 +215,20 @@ export const SelectedShapeActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
|
interactiveCanvas,
|
||||||
activeTool,
|
activeTool,
|
||||||
|
setAppState,
|
||||||
|
onImageAction,
|
||||||
appState,
|
appState,
|
||||||
app,
|
|
||||||
}: {
|
}: {
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
activeTool: UIAppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
app: AppClassProperties;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
|
const device = useDevice();
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
|
||||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
@@ -253,83 +253,156 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-${value}`}
|
data-testid={`toolbar-${value}`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
app.togglePenMode(true);
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
if (appState.activeTool.type !== value) {
|
if (appState.activeTool.type !== value) {
|
||||||
trackEvent("toolbar", value, "ui");
|
trackEvent("toolbar", value, "ui");
|
||||||
}
|
}
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: value,
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
activeEmbeddable: null,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
});
|
||||||
|
setCursorForShape(interactiveCanvas, {
|
||||||
|
...appState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
});
|
||||||
if (value === "image") {
|
if (value === "image") {
|
||||||
app.setActiveTool({
|
onImageAction({ pointerType });
|
||||||
type: value,
|
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
app.setActiveTool({ type: value });
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
|
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
{device.isMobile ? (
|
||||||
<DropdownMenu.Trigger
|
<>
|
||||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
<ToolButton
|
||||||
"App-toolbar__extra-tools-trigger--selected":
|
className={clsx("Shape", { fillable: false })}
|
||||||
frameToolSelected ||
|
type="radio"
|
||||||
embeddableToolSelected ||
|
|
||||||
// in collab we're already highlighting the laser button
|
|
||||||
// outside toolbar, so let's not highlight extra-tools button
|
|
||||||
// on top of it
|
|
||||||
(laserToolSelected && !app.props.isCollaborating),
|
|
||||||
})}
|
|
||||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
|
||||||
title={t("toolBar.extraTools")}
|
|
||||||
>
|
|
||||||
{extraToolsIcon}
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => app.setActiveTool({ type: "frame" })}
|
|
||||||
icon={frameToolIcon}
|
icon={frameToolIcon}
|
||||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
checked={activeTool.type === "frame"}
|
||||||
data-testid="toolbar-frame"
|
name="editor-current-shape"
|
||||||
selected={frameToolSelected}
|
title={`${capitalizeString(
|
||||||
>
|
t("toolBar.frame"),
|
||||||
{t("toolBar.frame")}
|
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||||
</DropdownMenu.Item>
|
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||||
<DropdownMenu.Item
|
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||||
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid={`toolbar-frame`}
|
||||||
|
onPointerDown={({ pointerType }) => {
|
||||||
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
trackEvent("toolbar", "frame", "ui");
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToolButton
|
||||||
|
className={clsx("Shape", { fillable: false })}
|
||||||
|
type="radio"
|
||||||
icon={EmbedIcon}
|
icon={EmbedIcon}
|
||||||
data-testid="toolbar-embeddable"
|
checked={activeTool.type === "embeddable"}
|
||||||
selected={embeddableToolSelected}
|
name="editor-current-shape"
|
||||||
|
title={capitalizeString(t("toolBar.embeddable"))}
|
||||||
|
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
||||||
|
data-testid={`toolbar-embeddable`}
|
||||||
|
onPointerDown={({ pointerType }) => {
|
||||||
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
|
setAppState({
|
||||||
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
trackEvent("toolbar", "embeddable", "ui");
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
className="App-toolbar__extra-tools-trigger"
|
||||||
|
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||||
|
title={t("toolBar.extraTools")}
|
||||||
>
|
>
|
||||||
{t("toolBar.embeddable")}
|
{extraToolsIcon}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Content
|
||||||
onSelect={() => app.setActiveTool({ type: "laser" })}
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
icon={laserPointerToolIcon}
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
data-testid="toolbar-laser"
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
selected={laserToolSelected}
|
|
||||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
|
||||||
>
|
>
|
||||||
{t("toolBar.laser")}
|
<DropdownMenu.Item
|
||||||
</DropdownMenu.Item>
|
onSelect={() => {
|
||||||
<DropdownMenu.Item
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
onSelect={() => app.setOpenDialog("mermaid")}
|
type: "frame",
|
||||||
icon={mermaidLogoIcon}
|
});
|
||||||
data-testid="toolbar-embeddable"
|
setAppState({
|
||||||
>
|
activeTool: nextActiveTool,
|
||||||
{t("toolBar.mermaidToExcalidraw")}
|
multiElement: null,
|
||||||
</DropdownMenu.Item>
|
selectedElementIds: {},
|
||||||
</DropdownMenu.Content>
|
});
|
||||||
</DropdownMenu>
|
}}
|
||||||
|
icon={frameToolIcon}
|
||||||
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid="toolbar-frame"
|
||||||
|
>
|
||||||
|
{t("toolBar.frame")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
});
|
||||||
|
setAppState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
multiElement: null,
|
||||||
|
selectedElementIds: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={EmbedIcon}
|
||||||
|
data-testid="toolbar-embeddable"
|
||||||
|
>
|
||||||
|
{t("toolBar.embeddable")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
<SubtypeToggles />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+299
-698
File diff suppressed because it is too large
Load Diff
@@ -98,7 +98,7 @@ export const ColorInput = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* TODO reenable on mobile with a better UX */}
|
{/* TODO reenable on mobile with a better UX */}
|
||||||
{!device.editor.isMobile && (
|
{!device.isMobile && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const ColorPickerPopupContent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
const device = useDevice();
|
const { isMobile, isLandscape } = useDevice();
|
||||||
|
|
||||||
const colorInputJSX = (
|
const colorInputJSX = (
|
||||||
<div>
|
<div>
|
||||||
@@ -136,16 +136,8 @@ const ColorPickerPopupContent = ({
|
|||||||
updateData({ openPopup: null });
|
updateData({ openPopup: null });
|
||||||
setActiveColorPickerSection(null);
|
setActiveColorPickerSection(null);
|
||||||
}}
|
}}
|
||||||
side={
|
side={isMobile && !isLandscape ? "bottom" : "right"}
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
align={isMobile && !isLandscape ? "center" : "start"}
|
||||||
? "bottom"
|
|
||||||
: "right"
|
|
||||||
}
|
|
||||||
align={
|
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
|
||||||
? "center"
|
|
||||||
: "start"
|
|
||||||
}
|
|
||||||
alignOffset={-16}
|
alignOffset={-16}
|
||||||
sideOffset={20}
|
sideOffset={20}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export const TopPicks = ({
|
|||||||
type="button"
|
type="button"
|
||||||
title={color}
|
title={color}
|
||||||
onClick={() => onChange(color)}
|
onClick={() => onChange(color)}
|
||||||
data-testid={`color-top-pick-${color}`}
|
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
import { Action } from "../actions/types";
|
import { Action } from "../actions/types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
|
import {
|
||||||
|
useExcalidrawAppState,
|
||||||
|
useExcalidrawElements,
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
} from "./App";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
||||||
@@ -21,14 +25,14 @@ type ContextMenuProps = {
|
|||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
onClose: (callback?: () => void) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONTEXT_MENU_SEPARATOR = "separator";
|
export const CONTEXT_MENU_SEPARATOR = "separator";
|
||||||
|
|
||||||
export const ContextMenu = React.memo(
|
export const ContextMenu = React.memo(
|
||||||
({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
|
({ actionManager, items, top, left }: ContextMenuProps) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const elements = useExcalidrawElements();
|
const elements = useExcalidrawElements();
|
||||||
|
|
||||||
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
||||||
@@ -50,9 +54,7 @@ export const ContextMenu = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onCloseRequest={() => {
|
onCloseRequest={() => setAppState({ contextMenu: null })}
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
top={top}
|
top={top}
|
||||||
left={left}
|
left={left}
|
||||||
fitInViewport={true}
|
fitInViewport={true}
|
||||||
@@ -100,7 +102,7 @@ export const ContextMenu = React.memo(
|
|||||||
// we need update state before executing the action in case
|
// we need update state before executing the action in case
|
||||||
// the action uses the appState it's being passed (that still
|
// the action uses the appState it's being passed (that still
|
||||||
// contains a defined contextMenu) to return the next state.
|
// contains a defined contextMenu) to return the next state.
|
||||||
onClose(() => {
|
setAppState({ contextMenu: null }, () => {
|
||||||
actionManager.executeAction(item, "contextMenu");
|
actionManager.executeAction(item, "contextMenu");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -33,16 +33,14 @@
|
|||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
top: 1.25rem;
|
||||||
|
right: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Dialog--fullscreen {
|
|
||||||
.Dialog__close {
|
|
||||||
top: 1.25rem;
|
|
||||||
right: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
const [lastActiveElement] = useState(document.activeElement);
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
const { id } = useExcalidrawContainer();
|
const { id } = useExcalidrawContainer();
|
||||||
const isFullscreen = useDevice().viewport.isMobile;
|
const device = useDevice();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@@ -101,9 +101,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className, {
|
className={clsx("Dialog", props.className)}
|
||||||
"Dialog--fullscreen": isFullscreen,
|
|
||||||
})}
|
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={getDialogSize(props.size)}
|
maxWidth={getDialogSize(props.size)}
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
@@ -121,7 +119,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
title={t("buttons.close")}
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{isFullscreen ? back : CloseIcon}
|
{device.isMobile ? back : CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
<div className="Dialog__content">{props.children}</div>
|
<div className="Dialog__content">{props.children}</div>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -12,32 +12,32 @@
|
|||||||
|
|
||||||
&--color-primary {
|
&--color-primary {
|
||||||
&.ExcButton--variant-filled {
|
&.ExcButton--variant-filled {
|
||||||
--text-color: var(--color-surface-lowest);
|
--text-color: var(--input-bg-color);
|
||||||
--back-color: var(--color-primary);
|
--back-color: var(--color-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--back-color: var(--color-brand-hover);
|
--back-color: var(--color-primary-darker);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
--back-color: var(--color-brand-active);
|
--back-color: var(--color-primary-darkest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ExcButton--variant-outlined,
|
&.ExcButton--variant-outlined,
|
||||||
&.ExcButton--variant-icon {
|
&.ExcButton--variant-icon {
|
||||||
--text-color: var(--color-primary);
|
--text-color: var(--color-primary);
|
||||||
--border-color: var(--color-border-outline);
|
--border-color: var(--color-primary);
|
||||||
--back-color: transparent;
|
--back-color: var(--input-bg-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--text-color: var(--color-brand-hover);
|
--text-color: var(--color-primary-darker);
|
||||||
--border-color: var(--color-brand-hover);
|
--border-color: var(--color-primary-darker);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
--text-color: var(--color-brand-active);
|
--text-color: var(--color-primary-darkest);
|
||||||
--border-color: var(--color-brand-active);
|
--border-color: var(--color-primary-darkest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,35 +19,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__btn {
|
&__btn {
|
||||||
--background: var(--color-surface-mid);
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background);
|
border: 1px solid var(--default-border-color);
|
||||||
padding: 0.625rem 1rem;
|
padding: 0.625rem 1rem;
|
||||||
border: 1px solid var(--background);
|
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
@at-root .excalidraw.theme--dark#{&} {
|
|
||||||
--background: var(--color-surface-high);
|
|
||||||
&:hover {
|
|
||||||
--background: #363541;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--background: var(--color-surface-high);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link-icon {
|
&__link-icon {
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
||||||
<Shortcut label={t("toolBar.laser")} shortcuts={[KEYS.K]} />
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.eyeDropper")}
|
label={t("labels.eyeDropper")}
|
||||||
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
||||||
@@ -254,14 +253,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("helpDialog.movePageLeftRight")}
|
label={t("helpDialog.movePageLeftRight")}
|
||||||
shortcuts={["Shift+PgUp/PgDn"]}
|
shortcuts={["Shift+PgUp/PgDn"]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("buttons.zenMode")}
|
label={t("buttons.zenMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
|
||||||
label={t("buttons.objectsSnapMode")}
|
|
||||||
shortcuts={[getShortcutKey("Alt+S")]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.showGrid")}
|
label={t("labels.showGrid")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
if (appState.openSidebar && !device.canDeviceFitSidebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas } from "../packages/utils";
|
import { exportToCanvas } from "../packages/utils";
|
||||||
|
|
||||||
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
||||||
@@ -34,8 +34,6 @@ import { Tooltip } from "./Tooltip";
|
|||||||
import "./ImageExportDialog.scss";
|
import "./ImageExportDialog.scss";
|
||||||
import { useAppProps } from "./App";
|
import { useAppProps } from "./App";
|
||||||
import { FilledButton } from "./FilledButton";
|
import { FilledButton } from "./FilledButton";
|
||||||
import { cloneJSON } from "../utils";
|
|
||||||
import { prepareElementsForExport } from "../data";
|
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@@ -53,47 +51,44 @@ export const ErrorCanvasPreview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ImageExportModalProps = {
|
type ImageExportModalProps = {
|
||||||
appStateSnapshot: Readonly<UIAppState>;
|
appState: UIAppState;
|
||||||
elementsSnapshot: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageExportModal = ({
|
const ImageExportModal = ({
|
||||||
appStateSnapshot,
|
appState,
|
||||||
elementsSnapshot,
|
elements,
|
||||||
files,
|
files,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportImage,
|
onExportImage,
|
||||||
}: ImageExportModalProps) => {
|
}: ImageExportModalProps) => {
|
||||||
const hasSelection = isSomeElementSelected(
|
|
||||||
elementsSnapshot,
|
|
||||||
appStateSnapshot,
|
|
||||||
);
|
|
||||||
|
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
const [projectName, setProjectName] = useState(appStateSnapshot.name);
|
const [projectName, setProjectName] = useState(appState.name);
|
||||||
const [exportSelectionOnly, setExportSelectionOnly] = useState(hasSelection);
|
|
||||||
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
|
|
||||||
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
const [exportWithBackground, setExportWithBackground] = useState(
|
const [exportWithBackground, setExportWithBackground] = useState(
|
||||||
appStateSnapshot.exportBackground,
|
appState.exportBackground,
|
||||||
);
|
);
|
||||||
const [exportDarkMode, setExportDarkMode] = useState(
|
const [exportDarkMode, setExportDarkMode] = useState(
|
||||||
appStateSnapshot.exportWithDarkMode,
|
appState.exportWithDarkMode,
|
||||||
);
|
);
|
||||||
const [embedScene, setEmbedScene] = useState(
|
const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene);
|
||||||
appStateSnapshot.exportEmbedScene,
|
const [exportScale, setExportScale] = useState(appState.exportScale);
|
||||||
);
|
|
||||||
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
|
|
||||||
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
const exportedElements = exportSelected
|
||||||
elementsSnapshot,
|
? getSelectedElements(elements, appState, {
|
||||||
appStateSnapshot,
|
includeBoundTextElement: true,
|
||||||
exportSelectionOnly,
|
includeElementsInFrames: true,
|
||||||
);
|
})
|
||||||
|
: elements;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previewNode = previewRef.current;
|
const previewNode = previewRef.current;
|
||||||
@@ -107,18 +102,10 @@ const ImageExportModal = ({
|
|||||||
}
|
}
|
||||||
exportToCanvas({
|
exportToCanvas({
|
||||||
elements: exportedElements,
|
elements: exportedElements,
|
||||||
appState: {
|
appState,
|
||||||
...appStateSnapshot,
|
|
||||||
name: projectName,
|
|
||||||
exportBackground: exportWithBackground,
|
|
||||||
exportWithDarkMode: exportDarkMode,
|
|
||||||
exportScale,
|
|
||||||
exportEmbedScene: embedScene,
|
|
||||||
},
|
|
||||||
files,
|
files,
|
||||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
||||||
exportingFrame,
|
|
||||||
})
|
})
|
||||||
.then((canvas) => {
|
.then((canvas) => {
|
||||||
setRenderError(null);
|
setRenderError(null);
|
||||||
@@ -132,17 +119,7 @@ const ImageExportModal = ({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
setRenderError(error);
|
setRenderError(error);
|
||||||
});
|
});
|
||||||
}, [
|
}, [appState, files, exportedElements]);
|
||||||
appStateSnapshot,
|
|
||||||
files,
|
|
||||||
exportedElements,
|
|
||||||
exportingFrame,
|
|
||||||
projectName,
|
|
||||||
exportWithBackground,
|
|
||||||
exportDarkMode,
|
|
||||||
exportScale,
|
|
||||||
embedScene,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ImageExportModal">
|
<div className="ImageExportModal">
|
||||||
@@ -159,8 +136,7 @@ const ImageExportModal = ({
|
|||||||
value={projectName}
|
value={projectName}
|
||||||
style={{ width: "30ch" }}
|
style={{ width: "30ch" }}
|
||||||
disabled={
|
disabled={
|
||||||
typeof appProps.name !== "undefined" ||
|
typeof appProps.name !== "undefined" || appState.viewModeEnabled
|
||||||
appStateSnapshot.viewModeEnabled
|
|
||||||
}
|
}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setProjectName(event.target.value);
|
setProjectName(event.target.value);
|
||||||
@@ -176,16 +152,16 @@ const ImageExportModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="ImageExportModal__settings">
|
<div className="ImageExportModal__settings">
|
||||||
<h3>{t("imageExportDialog.header")}</h3>
|
<h3>{t("imageExportDialog.header")}</h3>
|
||||||
{hasSelection && (
|
{someElementIsSelected && (
|
||||||
<ExportSetting
|
<ExportSetting
|
||||||
label={t("imageExportDialog.label.onlySelected")}
|
label={t("imageExportDialog.label.onlySelected")}
|
||||||
name="exportOnlySelected"
|
name="exportOnlySelected"
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
name="exportOnlySelected"
|
name="exportOnlySelected"
|
||||||
checked={exportSelectionOnly}
|
checked={exportSelected}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
setExportSelectionOnly(checked);
|
setExportSelected(checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ExportSetting>
|
</ExportSetting>
|
||||||
@@ -267,9 +243,7 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.exportToPng")}
|
label={t("imageExportDialog.title.exportToPng")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements, {
|
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||||
exportingFrame,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
startIcon={downloadIcon}
|
startIcon={downloadIcon}
|
||||||
>
|
>
|
||||||
@@ -279,9 +253,7 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.exportToSvg")}
|
label={t("imageExportDialog.title.exportToSvg")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements, {
|
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||||
exportingFrame,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
startIcon={downloadIcon}
|
startIcon={downloadIcon}
|
||||||
>
|
>
|
||||||
@@ -292,9 +264,7 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements, {
|
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||||
exportingFrame,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
startIcon={copyIcon}
|
startIcon={copyIcon}
|
||||||
>
|
>
|
||||||
@@ -355,20 +325,15 @@ export const ImageExportDialog = ({
|
|||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
// we need to take a snapshot so that the exported state can't be modified
|
if (appState.openDialog !== "imageExport") {
|
||||||
// while the dialog is open
|
return null;
|
||||||
const [{ appStateSnapshot, elementsSnapshot }] = useState(() => {
|
}
|
||||||
return {
|
|
||||||
appStateSnapshot: cloneJSON(appState),
|
|
||||||
elementsSnapshot: cloneJSON(elements),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
||||||
<ImageExportModal
|
<ImageExportModal
|
||||||
elementsSnapshot={elementsSnapshot}
|
elements={elements}
|
||||||
appStateSnapshot={appStateSnapshot}
|
appState={appState}
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportImage={onExportImage}
|
onExportImage={onExportImage}
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ export type ExportCB = (
|
|||||||
const JSONExportModal = ({
|
const JSONExportModal = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
setAppState,
|
|
||||||
files,
|
files,
|
||||||
actionManager,
|
actionManager,
|
||||||
exportOpts,
|
exportOpts,
|
||||||
canvas,
|
canvas,
|
||||||
onCloseRequest,
|
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -75,14 +72,9 @@ const JSONExportModal = ({
|
|||||||
title={t("exportDialog.link_button")}
|
title={t("exportDialog.link_button")}
|
||||||
aria-label={t("exportDialog.link_button")}
|
aria-label={t("exportDialog.link_button")}
|
||||||
showAriaLabel={true}
|
showAriaLabel={true}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
try {
|
onExportToBackend(elements, appState, files, canvas);
|
||||||
trackEvent("export", "link", `ui (${getFrame()})`);
|
trackEvent("export", "link", `ui (${getFrame()})`);
|
||||||
await onExportToBackend(elements, appState, files, canvas);
|
|
||||||
onCloseRequest();
|
|
||||||
} catch (error: any) {
|
|
||||||
setAppState({ errorMessage: error.message });
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -122,7 +114,6 @@ export const JSONExportDialog = ({
|
|||||||
<JSONExportModal
|
<JSONExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
setAppState={setAppState}
|
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
import { LaserPointer } from "@excalidraw/laser-pointer";
|
|
||||||
|
|
||||||
import { sceneCoordsToViewportCoords } from "../../utils";
|
|
||||||
import App from "../App";
|
|
||||||
import { getClientColor } from "../../clients";
|
|
||||||
|
|
||||||
// decay time in milliseconds
|
|
||||||
const DECAY_TIME = 1000;
|
|
||||||
// length of line in points before it starts decaying
|
|
||||||
const DECAY_LENGTH = 50;
|
|
||||||
|
|
||||||
const average = (a: number, b: number) => (a + b) / 2;
|
|
||||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
|
||||||
const len = points.length;
|
|
||||||
|
|
||||||
if (len < 4) {
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = points[0];
|
|
||||||
let b = points[1];
|
|
||||||
const c = points[2];
|
|
||||||
|
|
||||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
|
|
||||||
2,
|
|
||||||
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
|
|
||||||
b[1],
|
|
||||||
c[1],
|
|
||||||
).toFixed(2)} T`;
|
|
||||||
|
|
||||||
for (let i = 2, max = len - 1; i < max; i++) {
|
|
||||||
a = points[i];
|
|
||||||
b = points[i + 1];
|
|
||||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
|
|
||||||
2,
|
|
||||||
)} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closed) {
|
|
||||||
result += "Z";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
LPM: LaserPathManager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function easeOutCubic(t: number) {
|
|
||||||
return 1 - Math.pow(1 - t, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
function instantiateCollabolatorState(): CollabolatorState {
|
|
||||||
return {
|
|
||||||
currentPath: undefined,
|
|
||||||
finishedPaths: [],
|
|
||||||
lastPoint: [-10000, -10000],
|
|
||||||
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function instantiatePath() {
|
|
||||||
LaserPointer.constants.cornerDetectionMaxAngle = 70;
|
|
||||||
|
|
||||||
return new LaserPointer({
|
|
||||||
simplify: 0,
|
|
||||||
streamline: 0.4,
|
|
||||||
sizeMapping: (c) => {
|
|
||||||
const pt = DECAY_TIME;
|
|
||||||
const pl = DECAY_LENGTH;
|
|
||||||
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
|
|
||||||
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
|
|
||||||
|
|
||||||
return Math.min(easeOutCubic(l), easeOutCubic(t));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollabolatorState = {
|
|
||||||
currentPath: LaserPointer | undefined;
|
|
||||||
finishedPaths: LaserPointer[];
|
|
||||||
lastPoint: [number, number];
|
|
||||||
svg: SVGPathElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LaserPathManager {
|
|
||||||
private ownState: CollabolatorState;
|
|
||||||
private collaboratorsState: Map<string, CollabolatorState> = new Map();
|
|
||||||
|
|
||||||
private rafId: number | undefined;
|
|
||||||
private isDrawing = false;
|
|
||||||
private container: SVGSVGElement | undefined;
|
|
||||||
|
|
||||||
constructor(private app: App) {
|
|
||||||
this.ownState = instantiateCollabolatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.stop();
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.ownState = instantiateCollabolatorState();
|
|
||||||
this.collaboratorsState = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
startPath(x: number, y: number) {
|
|
||||||
this.ownState.currentPath = instantiatePath();
|
|
||||||
this.ownState.currentPath.addPoint([x, y, performance.now()]);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPointToPath(x: number, y: number) {
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endPath() {
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
this.ownState.currentPath.close();
|
|
||||||
this.ownState.finishedPaths.push(this.ownState.currentPath);
|
|
||||||
this.updatePath(this.ownState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updatePath(state: CollabolatorState) {
|
|
||||||
this.isDrawing = true;
|
|
||||||
|
|
||||||
if (!this.isRunning) {
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isRunning = false;
|
|
||||||
|
|
||||||
start(svg?: SVGSVGElement) {
|
|
||||||
if (svg) {
|
|
||||||
this.container = svg;
|
|
||||||
this.container.appendChild(this.ownState.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stop();
|
|
||||||
this.isRunning = true;
|
|
||||||
this.loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.isRunning = false;
|
|
||||||
if (this.rafId) {
|
|
||||||
cancelAnimationFrame(this.rafId);
|
|
||||||
}
|
|
||||||
this.rafId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
loop() {
|
|
||||||
this.rafId = requestAnimationFrame(this.loop.bind(this));
|
|
||||||
|
|
||||||
this.updateCollabolatorsState();
|
|
||||||
|
|
||||||
if (this.isDrawing) {
|
|
||||||
this.update();
|
|
||||||
} else {
|
|
||||||
this.isRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(path: LaserPointer) {
|
|
||||||
const stroke = path
|
|
||||||
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
|
|
||||||
.map(([x, y]) => {
|
|
||||||
const result = sceneCoordsToViewportCoords(
|
|
||||||
{ sceneX: x, sceneY: y },
|
|
||||||
this.app.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [result.x, result.y];
|
|
||||||
});
|
|
||||||
|
|
||||||
return getSvgPathFromStroke(stroke, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCollabolatorsState() {
|
|
||||||
if (!this.container || !this.app.state.collaborators.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
|
|
||||||
if (!this.collaboratorsState.has(key)) {
|
|
||||||
const state = instantiateCollabolatorState();
|
|
||||||
this.container.appendChild(state.svg);
|
|
||||||
this.collaboratorsState.set(key, state);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = this.collaboratorsState.get(key)!;
|
|
||||||
|
|
||||||
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
|
|
||||||
if (collabolator.button === "down" && state.currentPath === undefined) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath = instantiatePath();
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collabolator.button === "down" && state.currentPath !== undefined) {
|
|
||||||
if (
|
|
||||||
collabolator.pointer.x !== state.lastPoint[0] ||
|
|
||||||
collabolator.pointer.y !== state.lastPoint[1]
|
|
||||||
) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collabolator.button === "up" && state.currentPath !== undefined) {
|
|
||||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
||||||
state.currentPath.addPoint([
|
|
||||||
collabolator.pointer.x,
|
|
||||||
collabolator.pointer.y,
|
|
||||||
performance.now(),
|
|
||||||
]);
|
|
||||||
state.currentPath.close();
|
|
||||||
|
|
||||||
state.finishedPaths.push(state.currentPath);
|
|
||||||
state.currentPath = undefined;
|
|
||||||
|
|
||||||
this.updatePath(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let somePathsExist = false;
|
|
||||||
|
|
||||||
for (const [key, state] of this.collaboratorsState.entries()) {
|
|
||||||
if (!this.app.state.collaborators.has(key)) {
|
|
||||||
state.svg.remove();
|
|
||||||
this.collaboratorsState.delete(key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.finishedPaths = state.finishedPaths.filter((path) => {
|
|
||||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
||||||
|
|
||||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
|
|
||||||
|
|
||||||
if (state.currentPath) {
|
|
||||||
paths += ` ${this.draw(state.currentPath)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paths.trim()) {
|
|
||||||
somePathsExist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.svg.setAttribute("d", paths);
|
|
||||||
state.svg.setAttribute("fill", getClientColor(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
|
|
||||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
||||||
|
|
||||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paths = this.ownState.finishedPaths
|
|
||||||
.map((path) => this.draw(path))
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
if (this.ownState.currentPath) {
|
|
||||||
paths += ` ${this.draw(this.ownState.currentPath)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
paths = paths.trim();
|
|
||||||
|
|
||||||
if (paths) {
|
|
||||||
somePathsExist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ownState.svg.setAttribute("d", paths);
|
|
||||||
this.ownState.svg.setAttribute("fill", "red");
|
|
||||||
|
|
||||||
if (!somePathsExist) {
|
|
||||||
this.isDrawing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import "../ToolIcon.scss";
|
|
||||||
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { ToolButtonSize } from "../ToolButton";
|
|
||||||
import { laserPointerToolIcon } from "../icons";
|
|
||||||
|
|
||||||
type LaserPointerIconProps = {
|
|
||||||
title?: string;
|
|
||||||
name?: string;
|
|
||||||
checked: boolean;
|
|
||||||
onChange?(): void;
|
|
||||||
isMobile?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SIZE: ToolButtonSize = "small";
|
|
||||||
|
|
||||||
export const LaserPointerButton = (props: LaserPointerIconProps) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
className={clsx(
|
|
||||||
"ToolIcon ToolIcon__LaserPointer",
|
|
||||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
|
||||||
{
|
|
||||||
"is-mobile": props.isMobile,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
title={`${props.title}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="ToolIcon_type_checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
name={props.name}
|
|
||||||
onChange={props.onChange}
|
|
||||||
checked={props.checked}
|
|
||||||
aria-label={props.title}
|
|
||||||
data-testid="toolbar-LaserPointer"
|
|
||||||
/>
|
|
||||||
<div className="ToolIcon__icon">{laserPointerToolIcon}</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { LaserPathManager } from "./LaserPathManager";
|
|
||||||
import "./LaserToolOverlay.scss";
|
|
||||||
|
|
||||||
type LaserToolOverlayProps = {
|
|
||||||
manager: LaserPathManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
|
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (svgRef.current) {
|
|
||||||
manager.start(svgRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
manager.stop();
|
|
||||||
};
|
|
||||||
}, [manager]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="LaserToolOverlay">
|
|
||||||
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.excalidraw {
|
|
||||||
.LaserToolOverlay {
|
|
||||||
pointer-events: none;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.LaserToolOverlayCanvas {
|
|
||||||
image-rendering: auto;
|
|
||||||
overflow: visible;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+36
-38
@@ -55,28 +55,28 @@ import "./Toolbar.scss";
|
|||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
|
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
onPenModeToggle: () => void;
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
isCollaborating: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@@ -121,6 +121,7 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
|
interactiveCanvas,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@@ -128,11 +129,11 @@ const LayerUI = ({
|
|||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
|
onImageAction,
|
||||||
onExportImage,
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
app,
|
app,
|
||||||
isCollaborating,
|
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -161,10 +162,7 @@ const LayerUI = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderImageExportDialog = () => {
|
const renderImageExportDialog = () => {
|
||||||
if (
|
if (!UIOptions.canvasActions.saveAsImage) {
|
||||||
!UIOptions.canvasActions.saveAsImage ||
|
|
||||||
appState.openDialog !== "imageExport"
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +247,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
isMobile={device.editor.isMobile}
|
isMobile={device.isMobile}
|
||||||
device={device}
|
device={device}
|
||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
@@ -258,7 +256,7 @@ const LayerUI = ({
|
|||||||
<PenModeButton
|
<PenModeButton
|
||||||
zenModeEnabled={appState.zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={() => onPenModeToggle(null)}
|
onChange={onPenModeToggle}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
@@ -279,29 +277,17 @@ const LayerUI = ({
|
|||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
app={app}
|
setAppState={setAppState}
|
||||||
|
onImageAction={({ pointerType }) => {
|
||||||
|
onImageAction({
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
{isCollaborating && (
|
|
||||||
<Island
|
|
||||||
style={{
|
|
||||||
marginLeft: 8,
|
|
||||||
alignSelf: "center",
|
|
||||||
height: "fit-content",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LaserPointerButton
|
|
||||||
title={t("toolBar.laser")}
|
|
||||||
checked={appState.activeTool.type === "laser"}
|
|
||||||
onChange={() =>
|
|
||||||
app.setActiveTool({ type: "laser" })
|
|
||||||
}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
</Island>
|
|
||||||
)}
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +303,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserList collaborators={appState.collaborators} />
|
<UserList collaborators={appState.collaborators} />
|
||||||
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
{renderTopRightUI?.(device.isMobile, appState)}
|
||||||
{!appState.viewModeEnabled &&
|
{!appState.viewModeEnabled &&
|
||||||
// hide button when sidebar docked
|
// hide button when sidebar docked
|
||||||
(!isSidebarDocked ||
|
(!isSidebarDocked ||
|
||||||
@@ -338,7 +324,7 @@ const LayerUI = ({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"sidebar",
|
"sidebar",
|
||||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||||
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
`(${device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -366,7 +352,7 @@ const LayerUI = ({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"sidebar",
|
"sidebar",
|
||||||
`${DEFAULT_SIDEBAR.name} (open)`,
|
`${DEFAULT_SIDEBAR.name} (open)`,
|
||||||
`button (${device.editor.isMobile ? "mobile" : "desktop"})`,
|
`button (${device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -383,7 +369,7 @@ const LayerUI = ({
|
|||||||
{appState.errorMessage}
|
{appState.errorMessage}
|
||||||
</ErrorDialog>
|
</ErrorDialog>
|
||||||
)}
|
)}
|
||||||
{eyeDropperState && !device.editor.isMobile && (
|
{eyeDropperState && !device.isMobile && (
|
||||||
<EyeDropper
|
<EyeDropper
|
||||||
colorPickerType={eyeDropperState.colorPickerType}
|
colorPickerType={eyeDropperState.colorPickerType}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@@ -453,7 +439,7 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{device.editor.isMobile && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
app={app}
|
app={app}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -465,6 +451,8 @@ const LayerUI = ({
|
|||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
onHandToolToggle={onHandToolToggle}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
|
interactiveCanvas={interactiveCanvas}
|
||||||
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
@@ -472,14 +460,14 @@ const LayerUI = ({
|
|||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!device.editor.isMobile && (
|
{!device.isMobile && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="layer-ui__wrapper"
|
className="layer-ui__wrapper"
|
||||||
style={
|
style={
|
||||||
appState.openSidebar &&
|
appState.openSidebar &&
|
||||||
isSidebarDocked &&
|
isSidebarDocked &&
|
||||||
device.editor.canFitSidebar
|
device.canDeviceFitSidebar
|
||||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
@@ -551,8 +539,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
|
const {
|
||||||
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
|
canvas: _pC,
|
||||||
|
interactiveCanvas: _pIC,
|
||||||
|
appState: prevAppState,
|
||||||
|
...prev
|
||||||
|
} = prevProps;
|
||||||
|
const {
|
||||||
|
canvas: _nC,
|
||||||
|
interactiveCanvas: _nIC,
|
||||||
|
appState: nextAppState,
|
||||||
|
...next
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isShallowEqual(
|
isShallowEqual(
|
||||||
|
|||||||
@@ -99,10 +99,10 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-brand-hover);
|
background-color: var(--color-primary-darker);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--color-brand-active);
|
background-color: var(--color-primary-darkest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const LibraryUnit = memo(
|
|||||||
}, [svg]);
|
}, [svg]);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useDevice().editor.isMobile;
|
const isMobile = useDevice().isMobile;
|
||||||
const adder = isPending && (
|
const adder = isPending && (
|
||||||
<div className="library-unit__adder">{PlusIcon}</div>
|
<div className="library-unit__adder">{PlusIcon}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
@import "../css/variables.module";
|
|
||||||
|
|
||||||
$verticalBreakpoint: 860px;
|
|
||||||
|
|
||||||
.excalidraw {
|
|
||||||
.dialog-mermaid {
|
|
||||||
&-title {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
&-desc {
|
|
||||||
font-size: 15px;
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Modal__content .Island {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@at-root .excalidraw:not(.excalidraw--mobile)#{&} {
|
|
||||||
padding: 1.25rem;
|
|
||||||
|
|
||||||
.Modal__content {
|
|
||||||
height: 100%;
|
|
||||||
max-height: 750px;
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
height: auto;
|
|
||||||
// When vertical, we want the height to span whole viewport.
|
|
||||||
// This is also important for the children not to overflow the
|
|
||||||
// modal/viewport (for some reason).
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Island {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
.Dialog__content {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-mermaid-body {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr auto;
|
|
||||||
height: 100%;
|
|
||||||
column-gap: 4rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-mermaid-panels {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 4rem;
|
|
||||||
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
margin-left: 4px;
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 20rem;
|
|
||||||
height: 100%;
|
|
||||||
resize: none;
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: 1px solid var(--dialog-border-color);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
padding: 0.85rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
font-family: monospace;
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
width: auto;
|
|
||||||
height: 10rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-preview-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.85rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
// acts as min-height
|
|
||||||
height: 200px;
|
|
||||||
flex-grow: 1;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
|
||||||
left center;
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
border: 1px solid var(--dialog-border-color);
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
// acts as min-height
|
|
||||||
height: 400px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-preview-canvas-container {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-error {
|
|
||||||
color: red;
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 30px;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: Cascadia;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-mermaid-buttons {
|
|
||||||
grid-column: 2;
|
|
||||||
|
|
||||||
.dialog-mermaid-insert {
|
|
||||||
&.excalidraw-button {
|
|
||||||
font-family: "Assistant";
|
|
||||||
font-weight: 600;
|
|
||||||
height: 2.5rem;
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
width: 7.5rem;
|
|
||||||
font-size: 12px;
|
|
||||||
color: $oc-white;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: var(--color-primary-darkest);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $verticalBreakpoint) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@at-root .excalidraw.theme--dark#{&} {
|
|
||||||
color: var(--color-gray-100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, useDeferredValue } from "react";
|
|
||||||
import { BinaryFiles } from "../types";
|
|
||||||
import { useApp } from "./App";
|
|
||||||
import { Button } from "./Button";
|
|
||||||
import { Dialog } from "./Dialog";
|
|
||||||
import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
|
|
||||||
import {
|
|
||||||
convertToExcalidrawElements,
|
|
||||||
exportToCanvas,
|
|
||||||
} from "../packages/excalidraw/index";
|
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
|
||||||
import { canvasToBlob } from "../data/blob";
|
|
||||||
import { ArrowRightIcon } from "./icons";
|
|
||||||
import Spinner from "./Spinner";
|
|
||||||
import "./MermaidToExcalidraw.scss";
|
|
||||||
|
|
||||||
import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
|
||||||
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import Trans from "./Trans";
|
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
|
|
||||||
const MERMAID_EXAMPLE =
|
|
||||||
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
|
|
||||||
|
|
||||||
const saveMermaidDataToStorage = (data: string) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Unable to access window.localStorage
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const importMermaidDataFromStorage = () => {
|
|
||||||
try {
|
|
||||||
const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// Unable to access localStorage
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ErrorComp = ({ error }: { error: string }) => {
|
|
||||||
return (
|
|
||||||
<div data-testid="mermaid-error" className="mermaid-error">
|
|
||||||
Error! <p>{error}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MermaidToExcalidraw = () => {
|
|
||||||
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{
|
|
||||||
loaded: boolean;
|
|
||||||
api: {
|
|
||||||
parseMermaidToExcalidraw: (
|
|
||||||
defination: string,
|
|
||||||
options: MermaidOptions,
|
|
||||||
) => Promise<MermaidToExcalidrawResult>;
|
|
||||||
} | null;
|
|
||||||
}>({ loaded: false, api: null });
|
|
||||||
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const deferredText = useDeferredValue(text.trim());
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
|
||||||
const data = useRef<{
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
files: BinaryFiles | null;
|
|
||||||
}>({ elements: [], files: null });
|
|
||||||
|
|
||||||
const app = useApp();
|
|
||||||
|
|
||||||
const resetPreview = () => {
|
|
||||||
const canvasNode = canvasRef.current;
|
|
||||||
|
|
||||||
if (!canvasNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parent = canvasNode.parentElement;
|
|
||||||
if (!parent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parent.style.background = "";
|
|
||||||
setError(null);
|
|
||||||
canvasNode.replaceChildren();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadMermaidToExcalidrawLib = async () => {
|
|
||||||
const api = await import(
|
|
||||||
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
|
|
||||||
);
|
|
||||||
setMermaidToExcalidrawLib({ loaded: true, api });
|
|
||||||
};
|
|
||||||
loadMermaidToExcalidrawLib();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE;
|
|
||||||
setText(data);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const renderExcalidrawPreview = async () => {
|
|
||||||
const canvasNode = canvasRef.current;
|
|
||||||
const parent = canvasNode?.parentElement;
|
|
||||||
if (
|
|
||||||
!mermaidToExcalidrawLib.loaded ||
|
|
||||||
!canvasNode ||
|
|
||||||
!parent ||
|
|
||||||
!mermaidToExcalidrawLib.api
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!deferredText) {
|
|
||||||
resetPreview();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { elements, files } =
|
|
||||||
await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw(
|
|
||||||
deferredText,
|
|
||||||
{
|
|
||||||
fontSize: DEFAULT_FONT_SIZE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
data.current = {
|
|
||||||
elements: convertToExcalidrawElements(elements, {
|
|
||||||
regenerateIds: true,
|
|
||||||
}),
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
|
|
||||||
const canvas = await exportToCanvas({
|
|
||||||
elements: data.current.elements,
|
|
||||||
files: data.current.files,
|
|
||||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
|
||||||
maxWidthOrHeight:
|
|
||||||
Math.max(parent.offsetWidth, parent.offsetHeight) *
|
|
||||||
window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
// if converting to blob fails, there's some problem that will
|
|
||||||
// likely prevent preview and export (e.g. canvas too big)
|
|
||||||
await canvasToBlob(canvas);
|
|
||||||
parent.style.background = "var(--default-bg-color)";
|
|
||||||
canvasNode.replaceChildren(canvas);
|
|
||||||
} catch (e: any) {
|
|
||||||
parent.style.background = "var(--default-bg-color)";
|
|
||||||
if (deferredText) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
renderExcalidrawPreview();
|
|
||||||
}, [deferredText, mermaidToExcalidrawLib]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
app.setOpenDialog(null);
|
|
||||||
saveMermaidDataToStorage(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelect = () => {
|
|
||||||
const { elements: newElements, files } = data.current;
|
|
||||||
app.addElementsFromPasteOrLibrary({
|
|
||||||
elements: newElements,
|
|
||||||
files,
|
|
||||||
position: "center",
|
|
||||||
fitToContent: true,
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
className="dialog-mermaid"
|
|
||||||
onCloseRequest={onClose}
|
|
||||||
size={1200}
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<p className="dialog-mermaid-title">{t("mermaid.title")}</p>
|
|
||||||
<span className="dialog-mermaid-desc">
|
|
||||||
<Trans
|
|
||||||
i18nKey="mermaid.description"
|
|
||||||
flowchartLink={(el) => (
|
|
||||||
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
|
|
||||||
)}
|
|
||||||
sequenceLink={(el) => (
|
|
||||||
<a href="https://mermaid.js.org/syntax/sequenceDiagram.html">
|
|
||||||
{el}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="dialog-mermaid-body">
|
|
||||||
<div className="dialog-mermaid-panels">
|
|
||||||
<div className="dialog-mermaid-panels-text">
|
|
||||||
<label>{t("mermaid.syntax")}</label>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
onChange={(event) => setText(event.target.value)}
|
|
||||||
value={text}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="dialog-mermaid-panels-preview">
|
|
||||||
<label>{t("mermaid.preview")}</label>
|
|
||||||
<div className="dialog-mermaid-panels-preview-wrapper">
|
|
||||||
{error && <ErrorComp error={error} />}
|
|
||||||
{mermaidToExcalidrawLib.loaded ? (
|
|
||||||
<div
|
|
||||||
ref={canvasRef}
|
|
||||||
style={{ opacity: error ? "0.15" : 1 }}
|
|
||||||
className="dialog-mermaid-panels-preview-canvas-container"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Spinner size="2rem" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="dialog-mermaid-buttons">
|
|
||||||
<Button className="dialog-mermaid-insert" onSelect={onSelect}>
|
|
||||||
{t("mermaid.button")}
|
|
||||||
<span>{ArrowRightIcon}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default MermaidToExcalidraw;
|
|
||||||
@@ -35,8 +35,10 @@ type MobileMenuProps = {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: AppClassProperties["togglePenMode"];
|
onPenModeToggle: () => void;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
|
|
||||||
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@@ -56,7 +58,8 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
|
interactiveCanvas,
|
||||||
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
@@ -82,8 +85,14 @@ export const MobileMenu = ({
|
|||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
app={app}
|
setAppState={setAppState}
|
||||||
|
onImageAction={({ pointerType }) => {
|
||||||
|
onImageAction({
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
@@ -94,7 +103,7 @@ export const MobileMenu = ({
|
|||||||
)}
|
)}
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={() => onPenModeToggle(null)}
|
onChange={onPenModeToggle}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
isMobile
|
isMobile
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
max-width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes Modal__background__fade-in {
|
@keyframes Modal__background__fade-in {
|
||||||
@@ -99,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Dialog--fullscreen {
|
@include isMobile {
|
||||||
.Modal {
|
.Modal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -110,9 +116,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
max-width: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import { useApp } from "./App";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
|
||||||
import "./PasteChartDialog.scss";
|
import "./PasteChartDialog.scss";
|
||||||
|
import { ensureSubtypesLoaded } from "../element/subtypes";
|
||||||
|
import { isTextElement } from "../element";
|
||||||
|
import {
|
||||||
|
getContainerElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
} from "../element/textElement";
|
||||||
|
|
||||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||||
|
|
||||||
@@ -25,41 +31,54 @@ const ChartPreviewBtn = (props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!props.spreadsheet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = renderSpreadsheet(
|
|
||||||
props.chartType,
|
|
||||||
props.spreadsheet,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
setChartElements(elements);
|
|
||||||
let svg: SVGSVGElement;
|
let svg: SVGSVGElement;
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
svg = await exportToSvg(
|
(async () => {
|
||||||
elements,
|
let elements: ChartElements;
|
||||||
{
|
await ensureSubtypesLoaded(
|
||||||
exportBackground: false,
|
props.spreadsheet?.activeSubtypes ?? [],
|
||||||
viewBackgroundColor: oc.white,
|
() => {
|
||||||
},
|
if (!props.spreadsheet) {
|
||||||
null, // files
|
return;
|
||||||
);
|
}
|
||||||
svg.querySelector(".style-fonts")?.remove();
|
|
||||||
previewNode.replaceChildren();
|
|
||||||
previewNode.appendChild(svg);
|
|
||||||
|
|
||||||
if (props.selected) {
|
elements = renderSpreadsheet(
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
props.chartType,
|
||||||
}
|
props.spreadsheet,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
elements.forEach(
|
||||||
|
(el) =>
|
||||||
|
isTextElement(el) &&
|
||||||
|
redrawTextBoundingBox(el, getContainerElement(el)),
|
||||||
|
);
|
||||||
|
setChartElements(elements);
|
||||||
|
},
|
||||||
|
).then(async () => {
|
||||||
|
svg = await exportToSvg(
|
||||||
|
elements,
|
||||||
|
{
|
||||||
|
exportBackground: false,
|
||||||
|
viewBackgroundColor: oc.white,
|
||||||
|
},
|
||||||
|
null, // files
|
||||||
|
);
|
||||||
|
svg.querySelector(".style-fonts")?.remove();
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
|
if (props.selected) {
|
||||||
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewNode.replaceChildren();
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
previewNode.replaceChildren();
|
|
||||||
};
|
|
||||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--RadioGroup-background: var(--island-bg-color);
|
--RadioGroup-background: #ffffff;
|
||||||
--RadioGroup-border: var(--color-surface-high);
|
--RadioGroup-border: var(--color-gray-30);
|
||||||
|
|
||||||
--RadioGroup-choice-color-off: var(--color-primary);
|
--RadioGroup-choice-color-off: var(--color-primary);
|
||||||
--RadioGroup-choice-color-off-hover: var(--color-brand-hover);
|
--RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
|
||||||
--RadioGroup-choice-background-off: var(--island-bg-color);
|
--RadioGroup-choice-background-off: white;
|
||||||
--RadioGroup-choice-background-off-active: var(--color-surface-high);
|
--RadioGroup-choice-background-off-active: var(--color-gray-20);
|
||||||
|
|
||||||
--RadioGroup-choice-color-on: var(--color-surface-lowest);
|
--RadioGroup-choice-color-on: white;
|
||||||
--RadioGroup-choice-background-on: var(--color-primary);
|
--RadioGroup-choice-background-on: var(--color-primary);
|
||||||
--RadioGroup-choice-background-on-hover: var(--color-brand-hover);
|
--RadioGroup-choice-background-on-hover: var(--color-primary-darker);
|
||||||
--RadioGroup-choice-background-on-active: var(--color-brand-active);
|
--RadioGroup-choice-background-on-active: var(--color-primary-darkest);
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--RadioGroup-background: var(--color-gray-85);
|
||||||
|
--RadioGroup-border: var(--color-gray-70);
|
||||||
|
|
||||||
|
--RadioGroup-choice-background-off: var(--color-gray-85);
|
||||||
|
--RadioGroup-choice-background-off-active: var(--color-gray-70);
|
||||||
|
--RadioGroup-choice-color-on: var(--color-gray-85);
|
||||||
|
}
|
||||||
|
|
||||||
.RadioGroup {
|
.RadioGroup {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ export const SidebarInner = forwardRef(
|
|||||||
if ((event.target as Element).closest(".sidebar-trigger")) {
|
if ((event.target as Element).closest(".sidebar-trigger")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!docked || !device.editor.canFitSidebar) {
|
if (!docked || !device.canDeviceFitSidebar) {
|
||||||
closeLibrary();
|
closeLibrary();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeLibrary, docked, device.editor.canFitSidebar],
|
[closeLibrary, docked, device.canDeviceFitSidebar],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ export const SidebarInner = forwardRef(
|
|||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
event.key === KEYS.ESCAPE &&
|
event.key === KEYS.ESCAPE &&
|
||||||
(!docked || !device.editor.canFitSidebar)
|
(!docked || !device.canDeviceFitSidebar)
|
||||||
) {
|
) {
|
||||||
closeLibrary();
|
closeLibrary();
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ export const SidebarInner = forwardRef(
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [closeLibrary, docked, device.editor.canFitSidebar]);
|
}, [closeLibrary, docked, device.canDeviceFitSidebar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Island
|
<Island
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const SidebarHeader = ({
|
|||||||
const props = useContext(SidebarPropsContext);
|
const props = useContext(SidebarPropsContext);
|
||||||
|
|
||||||
const renderDockButton = !!(
|
const renderDockButton = !!(
|
||||||
device.editor.canFitSidebar && props.shouldRenderDockButton
|
device.canDeviceFitSidebar && props.shouldRenderDockButton
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.sidebar-trigger {
|
.sidebar-trigger {
|
||||||
@include outlineButtonStyles;
|
@include outlineButtonStyles;
|
||||||
@include filledButtonOnCanvas;
|
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
width: auto;
|
width: auto;
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { Action } from "../actions/types";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
Subtype,
|
||||||
|
getSubtypeNames,
|
||||||
|
hasAlwaysEnabledActions,
|
||||||
|
isSubtypeAction,
|
||||||
|
isValidSubtype,
|
||||||
|
subtypeCollides,
|
||||||
|
} from "../element/subtypes";
|
||||||
|
import { ExcalidrawElement, Theme } from "../element/types";
|
||||||
|
import {
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawContainer,
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
} from "./App";
|
||||||
|
import { ContextMenuItems } from "./ContextMenu";
|
||||||
|
|
||||||
|
export const SubtypeButton = (
|
||||||
|
subtype: Subtype,
|
||||||
|
parentType: ExcalidrawElement["type"],
|
||||||
|
icon: ({ theme }: { theme: Theme }) => JSX.Element,
|
||||||
|
key?: string,
|
||||||
|
) => {
|
||||||
|
const title = key !== undefined ? ` - ${getShortcutKey(key)}` : "";
|
||||||
|
const keyTest: Action["keyTest"] =
|
||||||
|
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
|
||||||
|
const subtypeAction: Action = {
|
||||||
|
name: subtype,
|
||||||
|
trackEvent: false,
|
||||||
|
predicate: (...rest) => rest[4]?.subtype === subtype,
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const inactive = !appState.activeSubtypes?.includes(subtype) ?? true;
|
||||||
|
const activeSubtypes: Subtype[] = [];
|
||||||
|
if (appState.activeSubtypes) {
|
||||||
|
activeSubtypes.push(...appState.activeSubtypes);
|
||||||
|
}
|
||||||
|
let activated = false;
|
||||||
|
if (inactive) {
|
||||||
|
// Ensure `element.subtype` is well-defined
|
||||||
|
if (!subtypeCollides(subtype, activeSubtypes)) {
|
||||||
|
activeSubtypes.push(subtype);
|
||||||
|
activated = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Can only be active if appState.activeSubtypes is defined
|
||||||
|
// and contains subtype.
|
||||||
|
activeSubtypes.splice(activeSubtypes.indexOf(subtype), 1);
|
||||||
|
}
|
||||||
|
const type =
|
||||||
|
appState.activeTool.type !== "custom" &&
|
||||||
|
isValidSubtype(subtype, appState.activeTool.type)
|
||||||
|
? appState.activeTool.type
|
||||||
|
: parentType;
|
||||||
|
const activeTool = !inactive
|
||||||
|
? appState.activeTool
|
||||||
|
: updateActiveTool(appState, { type });
|
||||||
|
const selectedElementIds = activated ? {} : appState.selectedElementIds;
|
||||||
|
const selectedGroupIds = activated ? {} : appState.selectedGroupIds;
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
activeSubtypes,
|
||||||
|
selectedElementIds,
|
||||||
|
selectedGroupIds,
|
||||||
|
activeTool,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest,
|
||||||
|
PanelComponent: ({ elements, appState, updateData, data }) => (
|
||||||
|
<button
|
||||||
|
className={clsx("ToolIcon_type_button", "ToolIcon_type_button--show", {
|
||||||
|
ToolIcon: true,
|
||||||
|
"ToolIcon--selected":
|
||||||
|
appState.activeSubtypes !== undefined &&
|
||||||
|
appState.activeSubtypes.includes(subtype),
|
||||||
|
"ToolIcon--plain": true,
|
||||||
|
})}
|
||||||
|
title={`${t(`toolBar.${subtype}`)}${title}`}
|
||||||
|
aria-label={t(`toolBar.${subtype}`)}
|
||||||
|
onClick={() => {
|
||||||
|
updateData(null);
|
||||||
|
}}
|
||||||
|
onContextMenu={
|
||||||
|
data && "onContextMenu" in data
|
||||||
|
? (event: React.MouseEvent) => {
|
||||||
|
if (
|
||||||
|
appState.activeSubtypes === undefined ||
|
||||||
|
(appState.activeSubtypes !== undefined &&
|
||||||
|
!appState.activeSubtypes.includes(subtype))
|
||||||
|
) {
|
||||||
|
updateData(null);
|
||||||
|
}
|
||||||
|
data.onContextMenu(event, subtype);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<div className="ToolIcon__icon" aria-hidden="true">
|
||||||
|
{icon.call(this, { theme: appState.theme })}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (key === "") {
|
||||||
|
delete subtypeAction.keyTest;
|
||||||
|
}
|
||||||
|
return subtypeAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubtypeToggles = () => {
|
||||||
|
const am = useExcalidrawActionManager();
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const onContextMenu = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
subtype: string,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { top: offsetTop, left: offsetLeft } =
|
||||||
|
container!.getBoundingClientRect();
|
||||||
|
const left = event.clientX - offsetLeft;
|
||||||
|
const top = event.clientY - offsetTop;
|
||||||
|
|
||||||
|
const items: ContextMenuItems = [];
|
||||||
|
am.filterActions(isSubtypeAction).forEach(
|
||||||
|
(action) =>
|
||||||
|
am.isActionEnabled(action, { data: { subtype } }) && items.push(action),
|
||||||
|
);
|
||||||
|
setAppState({}, () => {
|
||||||
|
setAppState({
|
||||||
|
contextMenu: { top, left, items },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getSubtypeNames().map((subtype) =>
|
||||||
|
am.renderAction(
|
||||||
|
subtype,
|
||||||
|
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SubtypeToggles.displayName = "SubtypeToggles";
|
||||||
|
|
||||||
|
export const SubtypeShapeActions = (props: {
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
}) => {
|
||||||
|
const am = useExcalidrawActionManager();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{am
|
||||||
|
.filterActions(isSubtypeAction, { elements: props.elements })
|
||||||
|
.map((action) => am.renderAction(action.name))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SubtypeShapeActions.displayName = "SubtypeShapeActions";
|
||||||
+14
-16
@@ -1,13 +1,15 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--Switch-disabled-color: var(--color-border-outline);
|
--Switch-disabled-color: #d6d6d6;
|
||||||
--Switch-disabled-toggled-background: var(--color-border-outline-variant);
|
--Switch-track-background: white;
|
||||||
--Switch-disabled-border: var(--color-border-outline-variant);
|
--Switch-thumb-background: #3d3d3d;
|
||||||
--Switch-track-background: var(--island-bg-color);
|
|
||||||
--Switch-thumb-background: var(--color-on-surface);
|
&.theme--dark {
|
||||||
--Switch-hover-background: var(--color-brand-hover);
|
--Switch-disabled-color: #5c5c5c;
|
||||||
--Switch-active-background: var(--color-brand-active);
|
--Switch-track-background: #242424;
|
||||||
|
--Switch-thumb-background: #b8b8b8;
|
||||||
|
}
|
||||||
|
|
||||||
.Switch {
|
.Switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -26,11 +28,7 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--Switch-track-background);
|
background: var(--Switch-track-background);
|
||||||
border: 1px solid var(--Switch-hover-background);
|
border: 1px solid #999999;
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border: 1px solid var(--Switch-active-background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.toggled {
|
&.toggled {
|
||||||
@@ -45,11 +43,11 @@
|
|||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background: var(--Switch-track-background);
|
background: var(--Switch-track-background);
|
||||||
border: 1px solid var(--Switch-disabled-border);
|
border: 1px solid var(--Switch-disabled-color);
|
||||||
|
|
||||||
&.toggled {
|
&.toggled {
|
||||||
background: var(--Switch-disabled-toggled-background);
|
background: var(--Switch-disabled-color);
|
||||||
border: 1px solid var(--Switch-disabled-toggled-background);
|
border: 1px solid var(--Switch-disabled-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.disabled.toggled:before {
|
&.disabled.toggled:before {
|
||||||
background: var(--Switch-disabled-color);
|
background: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--ExcTextField--color: var(--color-on-surface);
|
--ExcTextField--color: var(--color-gray-80);
|
||||||
--ExcTextField--label-color: var(--color-on-surface);
|
--ExcTextField--label-color: var(--color-gray-80);
|
||||||
--ExcTextField--background: transparent;
|
--ExcTextField--background: white;
|
||||||
--ExcTextField--readonly--background: var(--color-surface-high);
|
--ExcTextField--readonly--background: var(--color-gray-10);
|
||||||
--ExcTextField--readonly--color: var(--color-on-surface);
|
--ExcTextField--readonly--color: var(--color-gray-80);
|
||||||
--ExcTextField--border: var(--color-border-outline);
|
--ExcTextField--border: var(--color-gray-40);
|
||||||
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
--ExcTextField--border-hover: var(--color-gray-50);
|
||||||
--ExcTextField--border-hover: var(--color-brand-hover);
|
--ExcTextField--placeholder: var(--color-gray-40);
|
||||||
--ExcTextField--border-active: var(--color-brand-active);
|
|
||||||
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
&.theme--dark {
|
||||||
|
--ExcTextField--color: var(--color-gray-10);
|
||||||
|
--ExcTextField--label-color: var(--color-gray-20);
|
||||||
|
--ExcTextField--background: var(--color-gray-85);
|
||||||
|
--ExcTextField--readonly--background: var(--color-gray-80);
|
||||||
|
--ExcTextField--readonly--color: var(--color-gray-40);
|
||||||
|
--ExcTextField--border: var(--color-gray-70);
|
||||||
|
--ExcTextField--border-hover: var(--color-gray-60);
|
||||||
|
--ExcTextField--placeholder: var(--color-gray-80);
|
||||||
|
}
|
||||||
|
|
||||||
.ExcTextField {
|
.ExcTextField {
|
||||||
&--fullWidth {
|
&--fullWidth {
|
||||||
@@ -52,7 +61,7 @@
|
|||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border-color: var(--ExcTextField--border-active);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +107,7 @@
|
|||||||
|
|
||||||
&--readonly {
|
&--readonly {
|
||||||
background: var(--ExcTextField--readonly--background);
|
background: var(--ExcTextField--readonly--background);
|
||||||
border-color: var(--ExcTextField--readonly--border);
|
border-color: transparent;
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
color: var(--ExcTextField--readonly--color);
|
color: var(--ExcTextField--readonly--color);
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
isMountedRef.current = true;
|
() => () => {
|
||||||
return () => {
|
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
};
|
},
|
||||||
}, []);
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// background-color: var(--button-gray-2);
|
||||||
|
// }
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-gray-3);
|
background-color: var(--button-gray-3);
|
||||||
}
|
}
|
||||||
@@ -106,6 +110,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--hide {
|
&--hide {
|
||||||
|
// visibility: hidden;
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,24 +165,10 @@
|
|||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
|
||||||
width: 1.8rem;
|
|
||||||
height: 1.8rem;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 379px) {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__LaserPointer .ToolIcon__icon {
|
|
||||||
width: var(--default-button-size);
|
|
||||||
height: var(--default-button-size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,35 +16,23 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: var(--default-border-color);
|
background-color: var(--default-border-color);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
|
|
||||||
@include isMobile {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar__extra-tools-trigger {
|
.App-toolbar__extra-tools-trigger {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-hover-bg);
|
background-color: var(--button-hover-bg);
|
||||||
box-shadow: 0 0 0 1px
|
box-shadow: 0 0 0 1px
|
||||||
var(--button-active-border, var(--color-primary-darkest)) inset;
|
var(--button-active-border, var(--color-primary-darkest)) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected,
|
|
||||||
&--selected:hover {
|
|
||||||
background: var(--color-primary-light);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar__extra-tools-dropdown {
|
.App-toolbar__extra-tools-dropdown {
|
||||||
margin-top: 0.375rem;
|
margin-top: 0.375rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: 11.875rem;
|
min-width: 11.875rem;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,8 +193,6 @@ const getRelevantAppStateProps = (
|
|||||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||||
activeEmbeddable: appState.activeEmbeddable,
|
activeEmbeddable: appState.activeEmbeddable,
|
||||||
snapLines: appState.snapLines,
|
|
||||||
zenModeEnabled: appState.zenModeEnabled,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|||||||
@@ -114,13 +114,11 @@ const areEqual = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return isShallowEqual(
|
||||||
isShallowEqual(
|
// asserting AppState because we're being passed the whole AppState
|
||||||
// asserting AppState because we're being passed the whole AppState
|
// but resolve to only the StaticCanvas-relevant props
|
||||||
// but resolve to only the StaticCanvas-relevant props
|
getRelevantAppStateProps(prevProps.appState as AppState),
|
||||||
getRelevantAppStateProps(prevProps.appState as AppState),
|
getRelevantAppStateProps(nextProps.appState as AppState),
|
||||||
getRelevantAppStateProps(nextProps.appState as AppState),
|
|
||||||
) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
|
bottom: 55px;
|
||||||
|
top: auto;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
// background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
box-shadow: var(--shadow-island);
|
box-shadow: var(--shadow-island);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
background-color: var(--island-bg-color);
|
background-color: #fff !important;
|
||||||
max-height: calc(100vh - 150px);
|
max-height: calc(100vh - 150px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
--gap: 2;
|
--gap: 2;
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
padding: 0 0.625rem;
|
padding: 0 0.625rem;
|
||||||
column-gap: 0.625rem;
|
column-gap: 0.625rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-on-surface);
|
color: var(--color-gray-100);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
|
|
||||||
.dropdown-menu-item {
|
.dropdown-menu-item {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid transparent;
|
border: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -57,11 +59,6 @@
|
|||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background: var(--color-primary-light);
|
|
||||||
--icon-fill-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -78,11 +75,6 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-hover-bg);
|
|
||||||
border-color: var(--color-brand-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
@@ -101,33 +93,22 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.theme--dark {
|
||||||
|
.dropdown-menu-item {
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-container {
|
||||||
|
background-color: var(--color-gray-90) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu-button {
|
.dropdown-menu-button {
|
||||||
@include outlineButtonStyles;
|
@include outlineButtonStyles;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
--background: var(--color-surface-mid);
|
|
||||||
|
|
||||||
background-color: var(--background);
|
|
||||||
|
|
||||||
@at-root .excalidraw.theme--dark#{&} {
|
|
||||||
--background: var(--color-surface-high);
|
|
||||||
&:hover {
|
|
||||||
--background: #363541;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
--background: var(--color-surface-high);
|
|
||||||
background-color: var(--background);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const MenuContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||||
"dropdown-menu--mobile": device.editor.isMobile,
|
"dropdown-menu--mobile": device.isMobile,
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +43,7 @@ const MenuContent = ({
|
|||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
{device.editor.isMobile ? (
|
{device.isMobile ? (
|
||||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||||
) : (
|
) : (
|
||||||
<Island
|
<Island
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ const DropdownMenuItem = ({
|
|||||||
children,
|
children,
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
selected,
|
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
onSelect: (event: Event) => void;
|
onSelect: (event: Event) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
selected?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
@@ -28,7 +26,7 @@ const DropdownMenuItem = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const MenuItemContent = ({
|
|||||||
<>
|
<>
|
||||||
<div className="dropdown-menu-item__icon">{icon}</div>
|
<div className="dropdown-menu-item__icon">{icon}</div>
|
||||||
<div className="dropdown-menu-item__text">{children}</div>
|
<div className="dropdown-menu-item__text">{children}</div>
|
||||||
{shortcut && !device.editor.isMobile && (
|
{shortcut && !device.isMobile && (
|
||||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ import React from "react";
|
|||||||
const DropdownMenuItemCustom = ({
|
const DropdownMenuItemCustom = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
selected,
|
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
|
||||||
} & React.HTMLAttributes<HTMLDivElement>) => {
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${
|
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||||
selected ? `dropdown-menu-item--selected` : ``
|
|
||||||
}`.trim()}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const DropdownMenuItemLink = ({
|
|||||||
children,
|
children,
|
||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
selected,
|
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -20,7 +19,6 @@ const DropdownMenuItemLink = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
@@ -31,7 +29,7 @@ const DropdownMenuItemLink = ({
|
|||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const MenuTrigger = ({
|
|||||||
`dropdown-menu-button ${className}`,
|
`dropdown-menu-button ${className}`,
|
||||||
"zen-mode-transition",
|
"zen-mode-transition",
|
||||||
{
|
{
|
||||||
"dropdown-menu-button--mobile": device.editor.isMobile,
|
"dropdown-menu-button--mobile": device.isMobile,
|
||||||
},
|
},
|
||||||
).trim();
|
).trim();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,13 +6,8 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
|||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
export const getDropdownMenuItemClassName = (
|
export const getDropdownMenuItemClassName = (className = "") => {
|
||||||
className = "",
|
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||||
selected = false,
|
|
||||||
) => {
|
|
||||||
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
|
||||||
selected ? "dropdown-menu-item--selected" : ""
|
|
||||||
}`.trim();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleDropdownMenuItemClick = (
|
export const useHandleDropdownMenuItemClick = (
|
||||||
|
|||||||
@@ -1653,38 +1653,3 @@ export const frameToolIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const mermaidLogoIcon = createIcon(
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ArrowRightIcon = createIcon(
|
|
||||||
<g strokeWidth="1.25">
|
|
||||||
<path d="M4.16602 10H15.8327" />
|
|
||||||
<path d="M12.5 13.3333L15.8333 10" />
|
|
||||||
<path d="M12.5 6.66666L15.8333 9.99999" />
|
|
||||||
</g>,
|
|
||||||
modifiedTablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const laserPointerToolIcon = createIcon(
|
|
||||||
<g
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.25"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
transform="rotate(90 10 10)"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="m9.644 13.69 7.774-7.773a2.357 2.357 0 0 0-3.334-3.334l-7.773 7.774L8 12l1.643 1.69Z"
|
|
||||||
/>
|
|
||||||
<path d="m13.25 3.417 3.333 3.333M10 10l2-2M5 15l3-3M2.156 17.894l1-1M5.453 19.029l-.144-1.407M2.377 11.887l.866 1.118M8.354 17.273l-1.194-.758M.953 14.652l1.408.13" />
|
|
||||||
</g>,
|
|
||||||
|
|
||||||
20,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
|
|
||||||
--button-active-bg: var(--color-primary-darker);
|
--button-active-bg: var(--color-primary-darker);
|
||||||
|
|
||||||
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
// double .active to force specificity
|
// double .active to force specificity
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const MainMenu = Object.assign(
|
|||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const onClickOutside = device.editor.isMobile
|
const onClickOutside = device.isMobile
|
||||||
? undefined
|
? undefined
|
||||||
: () => setAppState({ openMenu: null });
|
: () => setAppState({ openMenu: null });
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ const MainMenu = Object.assign(
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
data-testid="main-menu-trigger"
|
data-testid="main-menu-trigger"
|
||||||
className="main-menu-trigger"
|
|
||||||
>
|
>
|
||||||
{HamburgerMenuIcon}
|
{HamburgerMenuIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
@@ -54,7 +53,7 @@ const MainMenu = Object.assign(
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
{device.isMobile && appState.collaborators.size > 0 && (
|
||||||
<fieldset className="UserList-Wrapper">
|
<fieldset className="UserList-Wrapper">
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
<UserList
|
<UserList
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const WelcomeScreenMenuItemContent = ({
|
|||||||
<>
|
<>
|
||||||
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||||
<div className="welcome-screen-menu-item__text">{children}</div>
|
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||||
{shortcut && !device.editor.isMobile && (
|
{shortcut && !device.isMobile && (
|
||||||
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid transparent;
|
border: none;
|
||||||
|
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
|
|
||||||
.welcome-screen-menu-item:hover {
|
.welcome-screen-menu-item:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--button-hover-bg);
|
background: var(--color-gray-10);
|
||||||
|
|
||||||
.welcome-screen-menu-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
@@ -216,8 +216,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-screen-menu-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background: var(--button-hover-bg);
|
background: var(--color-gray-20);
|
||||||
border-color: var(--color-brand-active);
|
|
||||||
|
|
||||||
.welcome-screen-menu-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
@@ -248,7 +247,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-screen-menu-item:hover {
|
.welcome-screen-menu-item:hover {
|
||||||
background-color: var(--color-surface-low);
|
background: var(--color-gray-85);
|
||||||
|
|
||||||
.welcome-screen-menu-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
@@ -259,6 +259,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-screen-menu-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
|
background-color: var(--color-gray-90);
|
||||||
.welcome-screen-menu-item__text {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-10);
|
color: var(--color-gray-10);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-24
@@ -105,7 +105,6 @@ export const FONT_FAMILY = {
|
|||||||
Virgil: 1,
|
Virgil: 1,
|
||||||
Helvetica: 2,
|
Helvetica: 2,
|
||||||
Cascadia: 3,
|
Cascadia: 3,
|
||||||
Assistant: 4,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
@@ -115,18 +114,13 @@ export const THEME = {
|
|||||||
|
|
||||||
export const FRAME_STYLE = {
|
export const FRAME_STYLE = {
|
||||||
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
||||||
strokeWidth: 2 as ExcalidrawElement["strokeWidth"],
|
strokeWidth: 1 as ExcalidrawElement["strokeWidth"],
|
||||||
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
|
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
|
||||||
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
|
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
|
||||||
roughness: 0 as ExcalidrawElement["roughness"],
|
roughness: 0 as ExcalidrawElement["roughness"],
|
||||||
roundness: null as ExcalidrawElement["roundness"],
|
roundness: null as ExcalidrawElement["roundness"],
|
||||||
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
|
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
|
||||||
radius: 8,
|
radius: 8,
|
||||||
nameOffsetY: 3,
|
|
||||||
nameColorLightTheme: "#999999",
|
|
||||||
nameColorDarkTheme: "#7a7a7a",
|
|
||||||
nameFontSize: 14,
|
|
||||||
nameLineHeight: 1.25,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
@@ -154,8 +148,6 @@ export const IMAGE_MIME_TYPES = {
|
|||||||
jfif: "image/jfif",
|
jfif: "image/jfif",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
// excalidraw data
|
// excalidraw data
|
||||||
@@ -226,6 +218,8 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
// sm screen
|
||||||
|
export const MQ_SM_MAX_WIDTH = 640;
|
||||||
// md screen
|
// md screen
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
@@ -302,18 +296,6 @@ export const ROUNDNESS = {
|
|||||||
* collaboration */
|
* collaboration */
|
||||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||||
|
|
||||||
export const ROUGHNESS = {
|
|
||||||
architect: 0,
|
|
||||||
artist: 1,
|
|
||||||
cartoonist: 2,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const STROKE_WIDTH = {
|
|
||||||
thin: 1,
|
|
||||||
bold: 2,
|
|
||||||
extraBold: 4,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const DEFAULT_ELEMENT_PROPS: {
|
export const DEFAULT_ELEMENT_PROPS: {
|
||||||
strokeColor: ExcalidrawElement["strokeColor"];
|
strokeColor: ExcalidrawElement["strokeColor"];
|
||||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||||
@@ -326,10 +308,10 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
} = {
|
} = {
|
||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
backgroundColor: COLOR_PALETTE.transparent,
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
fillStyle: "solid",
|
fillStyle: "hachure",
|
||||||
strokeWidth: 2,
|
strokeWidth: 1,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: ROUGHNESS.artist,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user