Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 502cee7b7a | |||
| e57dc405fa | |||
| 41ed019bc2 | |||
| f7c3644342 | |||
| 5e3550fc14 | |||
| 70888327a3 | |||
| 9fc15d81a0 | |||
| a80ac4c748 | |||
| 9f76f8677b | |||
| 2e46e27490 | |||
| cf0413338e | |||
| 49e4289878 |
@@ -0,0 +1,30 @@
|
|||||||
|
name: "Bundle Size check @excalidraw/excalidraw"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CI_JOB_NUMBER: 1
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js 18.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
- name: Install
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
- name: Install in src/packages/excalidraw
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: src/packages/excalidraw
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- uses: andresz1/size-limit-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build_script: build:umd
|
||||||
|
skip_step: install
|
||||||
|
directory: src/packages/excalidraw
|
||||||
@@ -165,3 +165,35 @@ function App() {
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
|
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
|
||||||
|
|
||||||
|
### MainMenu.Sub
|
||||||
|
|
||||||
|
The MainMenu component now supports submenus. To render a submenu, you can use `MainMenu.Sub`, `MainMenu.Sub.Trigger`, `MainMenu.Sub.Content` and `MainMenu.Sub.Item`. Note that `MainMenu.Sub.Trigger` and `MainMenu.Sub.Content` must be direct children of `MainMenu.Sub`.
|
||||||
|
|
||||||
|
```jsx live
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div style={{ height: "500px" }}>
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Submenu</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("Submenu item 1")}
|
||||||
|
>
|
||||||
|
Submenu item 1
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("Submenu item 2")}
|
||||||
|
>
|
||||||
|
Submenu item 2
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -9,14 +7,11 @@ export const actionAddToLibrary = register({
|
|||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selectedElements.some((element) => element.type === "image")) {
|
if (selectedElements.some((element) => element.type === "image")) {
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
|
|||||||
+32
-34
@@ -13,19 +13,18 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const alignActionsPredicate = (
|
const alignActionsPredicate = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
_: unknown,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length > 1 &&
|
selectedElements.length > 1 &&
|
||||||
// TODO enable aligning frames when implemented properly
|
// TODO enable aligning frames when implemented properly
|
||||||
@@ -36,12 +35,10 @@ const alignActionsPredicate = (
|
|||||||
const alignSelectedElements = (
|
const alignSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(selectedElements, alignment);
|
const updatedElements = alignElements(selectedElements, alignment);
|
||||||
|
|
||||||
@@ -50,6 +47,7 @@ const alignSelectedElements = (
|
|||||||
return updateFrameMembershipOfSelectedElements(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,10 +55,10 @@ export const actionAlignTop = register({
|
|||||||
name: "alignTop",
|
name: "alignTop",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -69,9 +67,9 @@ export const actionAlignTop = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -88,10 +86,10 @@ export const actionAlignBottom = register({
|
|||||||
name: "alignBottom",
|
name: "alignBottom",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -100,9 +98,9 @@ export const actionAlignBottom = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -119,10 +117,10 @@ export const actionAlignLeft = register({
|
|||||||
name: "alignLeft",
|
name: "alignLeft",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -131,9 +129,9 @@ export const actionAlignLeft = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -150,10 +148,10 @@ export const actionAlignRight = register({
|
|||||||
name: "alignRight",
|
name: "alignRight",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -162,9 +160,9 @@ export const actionAlignRight = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -181,19 +179,19 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
name: "alignVerticallyCentered",
|
name: "alignVerticallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -208,19 +206,19 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
name: "alignHorizontallyCentered",
|
name: "alignHorizontallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterHorizontallyIcon}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
import { isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
@@ -29,8 +29,8 @@ import {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -38,16 +38,13 @@ export const actionUnbindText = register({
|
|||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
contextItemLabel: "labels.unbindText",
|
contextItemLabel: "labels.unbindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
@@ -92,8 +89,8 @@ export const actionBindText = register({
|
|||||||
name: "bindText",
|
name: "bindText",
|
||||||
contextItemLabel: "labels.bindText",
|
contextItemLabel: "labels.bindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
if (selectedElements.length === 2) {
|
if (selectedElements.length === 2) {
|
||||||
const textElement =
|
const textElement =
|
||||||
@@ -116,11 +113,8 @@ export const actionBindText = register({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
let container: ExcalidrawTextContainer;
|
let container: ExcalidrawTextContainer;
|
||||||
@@ -200,18 +194,15 @@ export const actionWrapTextInContainer = register({
|
|||||||
name: "wrapTextInContainer",
|
name: "wrapTextInContainer",
|
||||||
contextItemLabel: "labels.createContainerFromText",
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||||
return selectedElements.length > 0 && areTextElements;
|
return selectedElements.length > 0 && areTextElements;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
const containerIds: AppState["selectedElementIds"] = {};
|
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||||
|
|
||||||
for (const textElement of selectedElements) {
|
for (const textElement of selectedElements) {
|
||||||
if (isTextElement(textElement)) {
|
if (isTextElement(textElement)) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
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";
|
||||||
@@ -302,11 +302,8 @@ export const zoomToFit = ({
|
|||||||
export const actionZoomToFitSelectionInViewport = register({
|
export const actionZoomToFitSelectionInViewport = register({
|
||||||
name: "zoomToFitSelectionInViewport",
|
name: "zoomToFitSelectionInViewport",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return zoomToFit({
|
return zoomToFit({
|
||||||
targetElements: selectedElements.length ? selectedElements : elements,
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
appState,
|
appState,
|
||||||
@@ -325,11 +322,8 @@ export const actionZoomToFitSelectionInViewport = register({
|
|||||||
export const actionZoomToFitSelection = register({
|
export const actionZoomToFitSelection = register({
|
||||||
name: "zoomToFitSelection",
|
name: "zoomToFitSelection",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return zoomToFit({
|
return zoomToFit({
|
||||||
targetElements: selectedElements.length ? selectedElements : elements,
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
appState,
|
appState,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { getSelectedElements } from "../scene/selection";
|
|
||||||
import { exportCanvas } from "../data/index";
|
import { exportCanvas } from "../data/index";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -16,7 +15,8 @@ export const actionCopy = register({
|
|||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const elementsToCopy = getSelectedElements(elements, appState, {
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -75,14 +75,11 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
@@ -122,14 +119,11 @@ export const actionCopyAsPng = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@@ -177,14 +171,11 @@ export const actionCopyAsPng = register({
|
|||||||
export const copyText = register({
|
export const copyText = register({
|
||||||
name: "copyText",
|
name: "copyText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = selectedElements
|
const text = selectedElements
|
||||||
.reduce((acc: string[], element) => {
|
.reduce((acc: string[], element) => {
|
||||||
@@ -199,12 +190,15 @@ export const copyText = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
getSelectedElements(elements, appState, {
|
app.scene
|
||||||
includeBoundTextElement: true,
|
.getSelectedElements({
|
||||||
}).some(isTextElement)
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
})
|
||||||
|
.some(isTextElement)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyText",
|
contextItemLabel: "labels.copyText",
|
||||||
|
|||||||
@@ -9,19 +9,13 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (
|
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const selectedElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length > 1 &&
|
selectedElements.length > 1 &&
|
||||||
// TODO enable distributing frames when implemented properly
|
// TODO enable distributing frames when implemented properly
|
||||||
@@ -32,12 +26,10 @@ const enableActionGroup = (
|
|||||||
const distributeSelectedElements = (
|
const distributeSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
distribution: Distribution,
|
distribution: Distribution,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = distributeElements(selectedElements, distribution);
|
const updatedElements = distributeElements(selectedElements, distribution);
|
||||||
|
|
||||||
@@ -46,16 +38,17 @@ const distributeSelectedElements = (
|
|||||||
return updateFrameMembershipOfSelectedElements(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const distributeHorizontally = register({
|
export const distributeHorizontally = register({
|
||||||
name: "distributeHorizontally",
|
name: "distributeHorizontally",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -64,9 +57,9 @@ export const distributeHorizontally = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeHorizontallyIcon}
|
icon={DistributeHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -82,10 +75,10 @@ export const distributeHorizontally = register({
|
|||||||
export const distributeVertically = register({
|
export const distributeVertically = register({
|
||||||
name: "distributeVertically",
|
name: "distributeVertically",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -94,9 +87,9 @@ export const distributeVertically = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeVerticallyIcon}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ const duplicateElements = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -11,14 +10,15 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
export const actionToggleElementLock = register({
|
export const actionToggleElementLock = register({
|
||||||
name: "toggleElementLock",
|
name: "toggleElementLock",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
return !selectedElements.some(
|
return !selectedElements.some(
|
||||||
(element) => element.locked && element.frameId,
|
(element) => element.locked && element.frameId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, {
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -46,8 +46,9 @@ export const actionToggleElementLock = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState, app) => {
|
||||||
const selected = getSelectedElements(elements, appState, {
|
const selected = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
});
|
});
|
||||||
if (selected.length === 1 && selected[0].type !== "frame") {
|
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||||
@@ -60,12 +61,13 @@ export const actionToggleElementLock = register({
|
|||||||
? "labels.elementLock.lockAll"
|
? "labels.elementLock.lockAll"
|
||||||
: "labels.elementLock.unlockAll";
|
: "labels.elementLock.unlockAll";
|
||||||
},
|
},
|
||||||
keyTest: (event, appState, elements) => {
|
keyTest: (event, appState, elements, app) => {
|
||||||
return (
|
return (
|
||||||
event.key.toLocaleLowerCase() === KEYS.L &&
|
event.key.toLocaleLowerCase() === KEYS.L &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
getSelectedElements(elements, appState, {
|
app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
}).length > 0
|
}).length > 0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
const scaleButtonTitle = `${t(
|
||||||
"buttons.scale",
|
"imageExportDialog.label.scale",
|
||||||
)} ${s}x (${width}x${height})`;
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
|||||||
checked={appState.exportBackground}
|
checked={appState.exportBackground}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.withBackground")}
|
{t("imageExportDialog.label.withBackground")}
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
checked={appState.exportEmbedScene}
|
checked={appState.exportEmbedScene}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.exportEmbedScene")}
|
{t("imageExportDialog.label.embedScene")}
|
||||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
@@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
|||||||
onChange={(theme: Theme) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === THEME.DARK);
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("imageExportDialog.label.darkMode")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -125,13 +125,6 @@ export const actionFinalize = register({
|
|||||||
{ x, y },
|
{ x, y },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!appState.activeTool.locked &&
|
|
||||||
appState.activeTool.type !== "freedraw"
|
|
||||||
) {
|
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
|
|||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "horizontal"),
|
flipSelectedElements(elements, appState, "horizontal"),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
@@ -34,11 +35,12 @@ export const actionFlipHorizontal = register({
|
|||||||
export const actionFlipVertical = register({
|
export const actionFlipVertical = register({
|
||||||
name: "flipVertical",
|
name: "flipVertical",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "vertical"),
|
flipSelectedElements(elements, appState, "vertical"),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
|||||||
+19
-27
@@ -3,19 +3,12 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameElements } from "../frame";
|
import { getFrameElements } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { AppState } from "../types";
|
|
||||||
import { setCursorForShape, updateActiveTool } from "../utils";
|
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const isSingleFrameSelected = (
|
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const selectedElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
||||||
};
|
};
|
||||||
@@ -23,11 +16,8 @@ const isSingleFrameSelected = (
|
|||||||
export const actionSelectAllElementsInFrame = register({
|
export const actionSelectAllElementsInFrame = register({
|
||||||
name: "selectAllElementsInFrame",
|
name: "selectAllElementsInFrame",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedFrame = getSelectedElements(
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameElements(
|
||||||
@@ -55,17 +45,15 @@ export const actionSelectAllElementsInFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.selectAllElementsInFrame",
|
contextItemLabel: "labels.selectAllElementsInFrame",
|
||||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionRemoveAllElementsFromFrame = register({
|
export const actionRemoveAllElementsFromFrame = register({
|
||||||
name: "removeAllElementsFromFrame",
|
name: "removeAllElementsFromFrame",
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedFrame = getSelectedElements(
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
return {
|
return {
|
||||||
@@ -87,11 +75,12 @@ export const actionRemoveAllElementsFromFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.removeAllElementsFromFrame",
|
contextItemLabel: "labels.removeAllElementsFromFrame",
|
||||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionToggleFrameRendering = register({
|
export const actionupdateFrameRendering = register({
|
||||||
name: "toggleFrameRendering",
|
name: "updateFrameRendering",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -99,13 +88,16 @@ export const actionToggleFrameRendering = register({
|
|||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
shouldRenderFrames: !appState.shouldRenderFrames,
|
frameRendering: {
|
||||||
|
...appState.frameRendering,
|
||||||
|
enabled: !appState.frameRendering.enabled,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.toggleFrameRendering",
|
contextItemLabel: "labels.updateFrameRendering",
|
||||||
checked: (appState: AppState) => appState.shouldRenderFrames,
|
checked: (appState: AppState) => appState.frameRendering.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSetFrameAsActiveTool = register({
|
export const actionSetFrameAsActiveTool = register({
|
||||||
|
|||||||
+29
-22
@@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { UngroupIcon, GroupIcon } from "../components/icons";
|
import { UngroupIcon, GroupIcon } from "../components/icons";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import {
|
import {
|
||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
selectGroup,
|
selectGroup,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
@@ -51,14 +51,12 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
const enableActionGroup = (
|
const enableActionGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
);
|
);
|
||||||
@@ -68,13 +66,10 @@ export const actionGroup = register({
|
|||||||
name: "group",
|
name: "group",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selectedElements.length < 2) {
|
if (selectedElements.length < 2) {
|
||||||
// nothing to group
|
// nothing to group
|
||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
@@ -164,12 +159,13 @@ export const actionGroup = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
enableActionGroup(elements, appState, app),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<GroupIcon theme={appState.theme} />}
|
icon={<GroupIcon theme={appState.theme} />}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -191,7 +187,7 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(nextElements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const frames = selectedElements
|
const frames = selectedElements
|
||||||
.filter((element) => element.frameId)
|
.filter((element) => element.frameId)
|
||||||
.map((element) =>
|
.map((element) =>
|
||||||
@@ -218,6 +214,8 @@ export const actionUngroup = register({
|
|||||||
const updateAppState = selectGroupsForSelectedElements(
|
const updateAppState = selectGroupsForSelectedElements(
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
@@ -232,9 +230,18 @@ export const actionUngroup = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// remove binded text elements from selection
|
// remove binded text elements from selection
|
||||||
boundTextElementIds.forEach(
|
updateAppState.selectedElementIds = Object.entries(
|
||||||
(id) => (updateAppState.selectedElementIds[id] = false),
|
updateAppState.selectedElementIds,
|
||||||
|
).reduce(
|
||||||
|
(acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
|
||||||
|
if (selected && !boundTextElementIds.includes(id)) {
|
||||||
|
acc[id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: updateAppState,
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleLinearEditor = register({
|
export const actionToggleLinearEditor = register({
|
||||||
@@ -10,21 +8,18 @@ export const actionToggleLinearEditor = register({
|
|||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform(elements, appState, _, app) {
|
perform(elements, appState, _, app) {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
})[0] as ExcalidrawLinearElement;
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
|
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
@@ -38,14 +33,11 @@ export const actionToggleLinearEditor = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState, app) => {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
})[0] as ExcalidrawLinearElement;
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
return appState.editingLinearElement?.elementId === selectedElement.id
|
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? "labels.lineEditor.exit"
|
? "labels.lineEditor.exit"
|
||||||
: "labels.lineEditor.edit";
|
: "labels.lineEditor.edit";
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const actionSelectAll = register({
|
|||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class ActionManager {
|
|||||||
event,
|
event,
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ export class ActionManager {
|
|||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
|
app={this.app}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export type ActionName =
|
|||||||
| "toggleHandTool"
|
| "toggleHandTool"
|
||||||
| "selectAllElementsInFrame"
|
| "selectAllElementsInFrame"
|
||||||
| "removeAllElementsFromFrame"
|
| "removeAllElementsFromFrame"
|
||||||
| "toggleFrameRendering"
|
| "updateFrameRendering"
|
||||||
| "setFrameAsActiveTool"
|
| "setFrameAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer";
|
| "wrapTextInContainer";
|
||||||
@@ -130,6 +130,7 @@ export type PanelComponentProps = {
|
|||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
|
app: AppClassProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
@@ -141,12 +142,14 @@ export interface Action {
|
|||||||
event: React.KeyboardEvent | KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
app: AppClassProperties,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
contextItemLabel?:
|
contextItemLabel?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
) => string);
|
) => string);
|
||||||
predicate?: (
|
predicate?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
shouldRenderFrames: true,
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
frameToHighlight: null,
|
frameToHighlight: null,
|
||||||
editingFrame: null,
|
editingFrame: null,
|
||||||
elementsToHighlight: null,
|
elementsToHighlight: null,
|
||||||
@@ -191,7 +191,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
showStats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
shouldRenderFrames: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
frameToHighlight: { browser: false, export: false, server: false },
|
frameToHighlight: { browser: false, export: false, server: false },
|
||||||
editingFrame: { browser: false, export: false, server: false },
|
editingFrame: { browser: false, export: false, server: false },
|
||||||
elementsToHighlight: { browser: false, export: false, server: false },
|
elementsToHighlight: { browser: false, export: false, server: false },
|
||||||
|
|||||||
+277
-218
@@ -315,7 +315,10 @@ import {
|
|||||||
updateFrameMembershipOfSelectedElements,
|
updateFrameMembershipOfSelectedElements,
|
||||||
isElementInFrame,
|
isElementInFrame,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
import {
|
||||||
|
excludeElementsInFramesFromSelection,
|
||||||
|
makeNextSelectedElementIds,
|
||||||
|
} from "../scene/selection";
|
||||||
import { actionPaste } from "../actions/actionClipboard";
|
import { actionPaste } from "../actions/actionClipboard";
|
||||||
import {
|
import {
|
||||||
actionRemoveAllElementsFromFrame,
|
actionRemoveAllElementsFromFrame,
|
||||||
@@ -327,6 +330,7 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -470,8 +474,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
name,
|
name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
showHyperlinkPopup: false,
|
|
||||||
defaultSidebarDockedPreference: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.id = nanoid();
|
this.id = nanoid();
|
||||||
@@ -502,7 +504,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setActiveTool: this.setActiveTool,
|
setActiveTool: this.setActiveTool,
|
||||||
setCursor: this.setCursor,
|
setCursor: this.setCursor,
|
||||||
resetCursor: this.resetCursor,
|
resetCursor: this.resetCursor,
|
||||||
toggleFrameRendering: this.toggleFrameRendering,
|
updateFrameRendering: this.updateFrameRendering,
|
||||||
toggleSidebar: this.toggleSidebar,
|
toggleSidebar: this.toggleSidebar,
|
||||||
} as const;
|
} as const;
|
||||||
if (typeof excalidrawRef === "function") {
|
if (typeof excalidrawRef === "function") {
|
||||||
@@ -648,7 +650,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private renderFrameNames = () => {
|
private renderFrameNames = () => {
|
||||||
if (!this.state.shouldRenderFrames) {
|
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,10 +798,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -856,6 +855,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.zenModeEnabled &&
|
!this.state.zenModeEnabled &&
|
||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
}
|
}
|
||||||
|
app={this}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
@@ -961,10 +961,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const shouldUpdateStrokeColor =
|
const shouldUpdateStrokeColor =
|
||||||
(type === "background" && event.altKey) ||
|
(type === "background" && event.altKey) ||
|
||||||
(type === "stroke" && !event.altKey);
|
(type === "stroke" && !event.altKey);
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getElementsIncludingDeleted(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
!selectedElements.length ||
|
!selectedElements.length ||
|
||||||
this.state.activeTool.type !== "selection"
|
this.state.activeTool.type !== "selection"
|
||||||
@@ -1353,6 +1350,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.destroy();
|
this.scene.destroy();
|
||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
|
isSomeElementSelected.clearCache();
|
||||||
touchTimeout = 0;
|
touchTimeout = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1825,7 +1823,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (event.touches.length === 2) {
|
if (event.touches.length === 2) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1835,7 +1833,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (event.touches.length > 0) {
|
if (event.touches.length > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
selectedElementIds: this.state.previousSelectedElementIds,
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
this.state.previousSelectedElementIds,
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
gesture.pointers.clear();
|
gesture.pointers.clear();
|
||||||
@@ -1895,7 +1896,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||||
this.insertImageElement(imageElement, file);
|
this.insertImageElement(imageElement, file);
|
||||||
this.initializeImageDimensions(imageElement);
|
this.initializeImageDimensions(imageElement);
|
||||||
this.setState({ selectedElementIds: { [imageElement.id]: true } });
|
this.setState({
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{
|
||||||
|
[imageElement.id]: true,
|
||||||
|
},
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2017,7 +2025,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
openSidebar:
|
openSidebar:
|
||||||
this.state.openSidebar &&
|
this.state.openSidebar &&
|
||||||
this.device.canDeviceFitSidebar &&
|
this.device.canDeviceFitSidebar &&
|
||||||
this.state.defaultSidebarDockedPreference
|
jotaiStore.get(isSidebarDockedAtom)
|
||||||
? this.state.openSidebar
|
? this.state.openSidebar
|
||||||
: null,
|
: null,
|
||||||
selectedElementIds: nextElementsToSelect.reduce(
|
selectedElementIds: nextElementsToSelect.reduce(
|
||||||
@@ -2032,6 +2040,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.state,
|
||||||
|
this,
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
if (opts.files) {
|
if (opts.files) {
|
||||||
@@ -2130,8 +2140,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: Object.fromEntries(
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
textElements.map((el) => [el.id, true]),
|
Object.fromEntries(textElements.map((el) => [el.id, true])),
|
||||||
|
this.state,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2192,10 +2203,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleFrameRendering = () => {
|
updateFrameRendering = (
|
||||||
|
opts:
|
||||||
|
| Partial<AppState["frameRendering"]>
|
||||||
|
| ((
|
||||||
|
prevState: AppState["frameRendering"],
|
||||||
|
) => Partial<AppState["frameRendering"]>),
|
||||||
|
) => {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
|
const next =
|
||||||
|
typeof opts === "function" ? opts(prevState.frameRendering) : opts;
|
||||||
return {
|
return {
|
||||||
shouldRenderFrames: !prevState.shouldRenderFrames,
|
frameRendering: {
|
||||||
|
enabled: next?.enabled ?? prevState.frameRendering.enabled,
|
||||||
|
clip: next?.clip ?? prevState.frameRendering.clip,
|
||||||
|
name: next?.name ?? prevState.frameRendering.name,
|
||||||
|
outline: next?.outline ?? prevState.frameRendering.outline,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -2582,14 +2606,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
offsetY = step;
|
offsetY = step;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements({
|
||||||
this.scene.getNonDeletedElements(),
|
selectedElementIds: this.state.selectedElementIds,
|
||||||
this.state,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -2606,10 +2627,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ENTER) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const selectedElement = selectedElements[0];
|
const selectedElement = selectedElements[0];
|
||||||
if (event[KEYS.CTRL_OR_CMD]) {
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
@@ -2685,10 +2703,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
!event[KEYS.CTRL_OR_CMD]
|
!event[KEYS.CTRL_OR_CMD]
|
||||||
) {
|
) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
this.state.activeTool.type === "selection" &&
|
this.state.activeTool.type === "selection" &&
|
||||||
!selectedElements.length
|
!selectedElements.length
|
||||||
@@ -2749,7 +2764,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.canvas, this.state);
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -2760,10 +2775,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ isBindingEnabled: true });
|
this.setState({ isBindingEnabled: true });
|
||||||
}
|
}
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
isBindingEnabled(this.state)
|
isBindingEnabled(this.state)
|
||||||
? bindOrUnbindSelectedElements(selectedElements)
|
? bindOrUnbindSelectedElements(selectedElements)
|
||||||
: unbindLinearElements(selectedElements);
|
: unbindLinearElements(selectedElements);
|
||||||
@@ -2794,7 +2806,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (nextActiveTool.type !== "selection") {
|
if (nextActiveTool.type !== "selection") {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -2831,7 +2843,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// elements by mistake while zooming
|
// elements by mistake while zooming
|
||||||
if (this.isTouchScreenMultiTouchGesture()) {
|
if (this.isTouchScreenMultiTouchGesture()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
gesture.initialScale = this.state.zoom.value;
|
gesture.initialScale = this.state.zoom.value;
|
||||||
@@ -2876,7 +2888,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (this.isTouchScreenMultiTouchGesture()) {
|
if (this.isTouchScreenMultiTouchGesture()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
selectedElementIds: this.state.previousSelectedElementIds,
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
this.state.previousSelectedElementIds,
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
gesture.initialScale = null;
|
gesture.initialScale = null;
|
||||||
@@ -2941,10 +2956,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? element.containerId
|
? element.containerId
|
||||||
: element.id;
|
: element.id;
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
...prevState.selectedElementIds,
|
{
|
||||||
[elementIdToSelect]: true,
|
...prevState.selectedElementIds,
|
||||||
},
|
[elementIdToSelect]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
@@ -2980,7 +2998,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private deselectElements() {
|
private deselectElements() {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -3067,7 +3085,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
).filter((element) => {
|
).filter((element) => {
|
||||||
// hitting a frame's element from outside the frame is not considered a hit
|
// hitting a frame's element from outside the frame is not considered a hit
|
||||||
const containingFrame = getContainingFrame(element);
|
const containingFrame = getContainingFrame(element);
|
||||||
return containingFrame && this.state.shouldRenderFrames
|
return containingFrame &&
|
||||||
|
this.state.frameRendering.enabled &&
|
||||||
|
this.state.frameRendering.clip
|
||||||
? isCursorInFrame({ x, y }, containingFrame)
|
? isCursorInFrame({ x, y }, containingFrame)
|
||||||
: true;
|
: true;
|
||||||
});
|
});
|
||||||
@@ -3105,10 +3125,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
if (isTextElement(selectedElements[0])) {
|
if (isTextElement(selectedElements[0])) {
|
||||||
@@ -3238,10 +3255,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
if (
|
if (
|
||||||
@@ -3291,6 +3305,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -3667,7 +3683,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
if (
|
if (
|
||||||
selectedElements.length === 1 &&
|
selectedElements.length === 1 &&
|
||||||
!isOverScrollBar &&
|
!isOverScrollBar &&
|
||||||
@@ -3998,12 +4014,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
editingElement: null,
|
editingElement: null,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
selectedElementIds: Object.keys(this.state.selectedElementIds)
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
.filter((key) => key !== element.id)
|
Object.keys(this.state.selectedElementIds)
|
||||||
.reduce((obj: { [id: string]: boolean }, key) => {
|
.filter((key) => key !== element.id)
|
||||||
obj[key] = this.state.selectedElementIds[key];
|
.reduce((obj: { [id: string]: true }, key) => {
|
||||||
return obj;
|
obj[key] = this.state.selectedElementIds[key];
|
||||||
}, {}),
|
return obj;
|
||||||
|
}, {}),
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -4367,10 +4386,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
): PointerDownState {
|
): PointerDownState {
|
||||||
const origin = viewportCoordsToSceneCoords(event, this.state);
|
const origin = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -4472,7 +4488,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private clearSelectionIfNotUsingSelection = (): void => {
|
private clearSelectionIfNotUsingSelection = (): void => {
|
||||||
if (this.state.activeTool.type !== "selection") {
|
if (this.state.activeTool.type !== "selection") {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -4488,7 +4504,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
if (this.state.activeTool.type === "selection") {
|
if (this.state.activeTool.type === "selection") {
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
const selectedElements = getSelectedElements(elements, this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
||||||
const elementWithTransformHandleType =
|
const elementWithTransformHandleType =
|
||||||
getElementWithTransformHandleType(
|
getElementWithTransformHandleType(
|
||||||
@@ -4604,9 +4620,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (this.state.editingLinearElement) {
|
if (this.state.editingLinearElement) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
[this.state.editingLinearElement.elementId]: true,
|
{
|
||||||
},
|
[this.state.editingLinearElement.elementId]: true,
|
||||||
|
},
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
// If we click on something
|
// If we click on something
|
||||||
} else if (hitElement != null) {
|
} else if (hitElement != null) {
|
||||||
@@ -4634,7 +4653,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElementInGroup(hitElement, this.state.editingGroupId)
|
!isElementInGroup(hitElement, this.state.editingGroupId)
|
||||||
) {
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -4650,7 +4669,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
) {
|
) {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const nextSelectedElementIds = {
|
const nextSelectedElementIds: { [id: string]: true } = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[hitElement.id]: true,
|
[hitElement.id]: true,
|
||||||
};
|
};
|
||||||
@@ -4668,13 +4687,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
previouslySelectedElements,
|
previouslySelectedElements,
|
||||||
hitElement.id,
|
hitElement.id,
|
||||||
).forEach((element) => {
|
).forEach((element) => {
|
||||||
nextSelectedElementIds[element.id] = false;
|
delete nextSelectedElementIds[element.id];
|
||||||
});
|
});
|
||||||
} else if (hitElement.frameId) {
|
} else if (hitElement.frameId) {
|
||||||
// if hitElement is in a frame and its frame has been selected
|
// if hitElement is in a frame and its frame has been selected
|
||||||
// disable selection for the given element
|
// disable selection for the given element
|
||||||
if (nextSelectedElementIds[hitElement.frameId]) {
|
if (nextSelectedElementIds[hitElement.frameId]) {
|
||||||
nextSelectedElementIds[hitElement.id] = false;
|
delete nextSelectedElementIds[hitElement.id];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// hitElement is neither a frame nor an element in a frame
|
// hitElement is neither a frame nor an element in a frame
|
||||||
@@ -4704,7 +4723,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
framesInGroups.has(element.frameId)
|
framesInGroups.has(element.frameId)
|
||||||
) {
|
) {
|
||||||
// deselect element and groups containing the element
|
// deselect element and groups containing the element
|
||||||
nextSelectedElementIds[element.id] = false;
|
delete nextSelectedElementIds[element.id];
|
||||||
element.groupIds
|
element.groupIds
|
||||||
.flatMap((gid) =>
|
.flatMap((gid) =>
|
||||||
getElementsInGroup(
|
getElementsInGroup(
|
||||||
@@ -4712,10 +4731,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
gid,
|
gid,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.forEach(
|
.forEach((element) => {
|
||||||
(element) =>
|
delete nextSelectedElementIds[element.id];
|
||||||
(nextSelectedElementIds[element.id] = false),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4728,6 +4746,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
showHyperlinkPopup: hitElement.link ? "info" : false,
|
showHyperlinkPopup: hitElement.link ? "info" : false,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
pointerDownState.hit.wasAddedToSelection = true;
|
pointerDownState.hit.wasAddedToSelection = true;
|
||||||
@@ -4844,12 +4864,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => {
|
||||||
selectedElementIds: {
|
const nextSelectedElementIds = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[element.id]: false,
|
};
|
||||||
},
|
delete nextSelectedElementIds[element.id];
|
||||||
}));
|
return {
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextSelectedElementIds,
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const pressures = element.simulatePressure
|
const pressures = element.simulatePressure
|
||||||
? element.pressures
|
? element.pressures
|
||||||
@@ -4945,10 +4971,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
...prevState.selectedElementIds,
|
{
|
||||||
[multiElement.id]: true,
|
...prevState.selectedElementIds,
|
||||||
},
|
[multiElement.id]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
// clicking outside commit zone → update reference for last committed
|
// clicking outside commit zone → update reference for last committed
|
||||||
// point
|
// point
|
||||||
@@ -4999,12 +5028,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => {
|
||||||
selectedElementIds: {
|
const nextSelectedElementIds = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[element.id]: false,
|
};
|
||||||
},
|
delete nextSelectedElementIds[element.id];
|
||||||
}));
|
return {
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextSelectedElementIds,
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [...element.points, [0, 0]],
|
points: [...element.points, [0, 0]],
|
||||||
});
|
});
|
||||||
@@ -5140,7 +5175,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (pointerDownState.drag.offset === null) {
|
if (pointerDownState.drag.offset === null) {
|
||||||
pointerDownState.drag.offset = tupleToCoors(
|
pointerDownState.drag.offset = tupleToCoors(
|
||||||
getDragOffsetXY(
|
getDragOffsetXY(
|
||||||
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
|
this.scene.getSelectedElements(this.state),
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
),
|
),
|
||||||
@@ -5303,10 +5338,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||||
!isSelectingPointsInLineEditor
|
!isSelectingPointsInLineEditor
|
||||||
) {
|
) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedElements.every((element) => element.locked)) {
|
if (selectedElements.every((element) => element.locked)) {
|
||||||
return;
|
return;
|
||||||
@@ -5377,16 +5409,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const oldIdToDuplicatedId = new Map();
|
const oldIdToDuplicatedId = new Map();
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hitElement = pointerDownState.hit.element;
|
||||||
const elements = this.scene.getElementsIncludingDeleted();
|
const selectedElementIds = new Set(
|
||||||
const selectedElementIds: Array<ExcalidrawElement["id"]> =
|
this.scene
|
||||||
getSelectedElements(elements, this.state, {
|
.getSelectedElements({
|
||||||
includeBoundTextElement: true,
|
selectedElementIds: this.state.selectedElementIds,
|
||||||
includeElementsInFrames: true,
|
includeBoundTextElement: true,
|
||||||
}).map((element) => element.id);
|
includeElementsInFrames: true,
|
||||||
|
})
|
||||||
|
.map((element) => element.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (
|
if (
|
||||||
selectedElementIds.includes(element.id) ||
|
selectedElementIds.has(element.id) ||
|
||||||
// case: the state.selectedElementIds might not have been
|
// case: the state.selectedElementIds might not have been
|
||||||
// updated yet by the time this mousemove event is fired
|
// updated yet by the time this mousemove event is fired
|
||||||
(element.id === hitElement?.id &&
|
(element.id === hitElement?.id &&
|
||||||
@@ -5524,14 +5561,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
selectedElementIds: {},
|
|
||||||
selectedGroupIds: {},
|
|
||||||
editingGroupId: null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// box-select line editor points
|
// box-select line editor points
|
||||||
@@ -5547,28 +5580,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements,
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
);
|
);
|
||||||
this.setState((prevState) =>
|
this.setState((prevState) => {
|
||||||
selectGroupsForSelectedElements(
|
const nextSelectedElementIds = elementsWithinSelection.reduce(
|
||||||
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
|
acc[element.id] = true;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pointerDownState.hit.element) {
|
||||||
|
// if using ctrl/cmd, select the hitElement only if we
|
||||||
|
// haven't box-selected anything else
|
||||||
|
if (!elementsWithinSelection.length) {
|
||||||
|
nextSelectedElementIds[pointerDownState.hit.element.id] = true;
|
||||||
|
} else {
|
||||||
|
delete nextSelectedElementIds[pointerDownState.hit.element.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
...prevState,
|
...prevState,
|
||||||
selectedElementIds: {
|
selectedElementIds: nextSelectedElementIds,
|
||||||
...prevState.selectedElementIds,
|
|
||||||
...elementsWithinSelection.reduce(
|
|
||||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
|
||||||
acc[element.id] = true;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
...(pointerDownState.hit.element
|
|
||||||
? {
|
|
||||||
// if using ctrl/cmd, select the hitElement only if we
|
|
||||||
// haven't box-selected anything else
|
|
||||||
[pointerDownState.hit.element.id]:
|
|
||||||
!elementsWithinSelection.length,
|
|
||||||
}
|
|
||||||
: null),
|
|
||||||
},
|
|
||||||
showHyperlinkPopup:
|
showHyperlinkPopup:
|
||||||
elementsWithinSelection.length === 1 &&
|
elementsWithinSelection.length === 1 &&
|
||||||
elementsWithinSelection[0].link
|
elementsWithinSelection[0].link
|
||||||
@@ -5585,8 +5619,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
),
|
prevState,
|
||||||
);
|
this,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -5684,10 +5720,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.hit?.element?.id !==
|
pointerDownState.hit?.element?.id !==
|
||||||
this.state.selectedLinearElement.elementId
|
this.state.selectedLinearElement.elementId
|
||||||
) {
|
) {
|
||||||
const selectedELements = getSelectedElements(
|
const selectedELements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
|
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
|
||||||
if (selectedELements.length > 1) {
|
if (selectedELements.length > 1) {
|
||||||
this.setState({ selectedLinearElement: null });
|
this.setState({ selectedLinearElement: null });
|
||||||
@@ -5780,7 +5813,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
try {
|
try {
|
||||||
this.initializeImageDimensions(imageElement);
|
this.initializeImageDimensions(imageElement);
|
||||||
this.setState(
|
this.setState(
|
||||||
{ selectedElementIds: { [imageElement.id]: true } },
|
{
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{ [imageElement.id]: true },
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
},
|
||||||
() => {
|
() => {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
},
|
},
|
||||||
@@ -5844,10 +5882,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, {
|
||||||
type: "selection",
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
...prevState.selectedElementIds,
|
{
|
||||||
[draggingElement.id]: true,
|
...prevState.selectedElementIds,
|
||||||
},
|
[draggingElement.id]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
selectedLinearElement: new LinearElementEditor(
|
selectedLinearElement: new LinearElementEditor(
|
||||||
draggingElement,
|
draggingElement,
|
||||||
this.scene,
|
this.scene,
|
||||||
@@ -5921,10 +5962,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const topLayerFrame =
|
const topLayerFrame =
|
||||||
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
let nextElements = this.scene.getElementsIncludingDeleted();
|
let nextElements = this.scene.getElementsIncludingDeleted();
|
||||||
|
|
||||||
const updateGroupIdsAfterEditingGroup = (
|
const updateGroupIdsAfterEditingGroup = (
|
||||||
@@ -6003,6 +6041,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
nextElements = updateFrameMembershipOfSelectedElements(
|
nextElements = updateFrameMembershipOfSelectedElements(
|
||||||
this.scene.getElementsIncludingDeleted(),
|
this.scene.getElementsIncludingDeleted(),
|
||||||
this.state,
|
this.state,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
@@ -6047,14 +6086,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let nextElements = updateFrameMembershipOfSelectedElements(
|
let nextElements = updateFrameMembershipOfSelectedElements(
|
||||||
this.scene.getElementsIncludingDeleted(),
|
this.scene.getElementsIncludingDeleted(),
|
||||||
this.state,
|
this.state,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedFrames = getSelectedElements(
|
const selectedFrames = this.scene
|
||||||
this.scene.getElementsIncludingDeleted(),
|
.getSelectedElements(this.state)
|
||||||
this.state,
|
.filter(
|
||||||
).filter(
|
(element) => element.type === "frame",
|
||||||
(element) => element.type === "frame",
|
) as ExcalidrawFrameElement[];
|
||||||
) as ExcalidrawFrameElement[];
|
|
||||||
|
|
||||||
for (const frame of selectedFrames) {
|
for (const frame of selectedFrames) {
|
||||||
nextElements = replaceAllElementsInFrame(
|
nextElements = replaceAllElementsInFrame(
|
||||||
@@ -6079,10 +6118,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
||||||
isLinearElement(hitElement)
|
isLinearElement(hitElement)
|
||||||
) {
|
) {
|
||||||
const selectedELements = getSelectedElements(
|
const selectedELements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
// set selectedLinearElement when no other element selected except
|
// set selectedLinearElement when no other element selected except
|
||||||
// the one we've hit
|
// the one we've hit
|
||||||
if (selectedELements.length === 1) {
|
if (selectedELements.length === 1) {
|
||||||
@@ -6141,31 +6177,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (childEvent.shiftKey && !this.state.editingLinearElement) {
|
if (childEvent.shiftKey && !this.state.editingLinearElement) {
|
||||||
if (this.state.selectedElementIds[hitElement.id]) {
|
if (this.state.selectedElementIds[hitElement.id]) {
|
||||||
if (isSelectedViaGroup(this.state, hitElement)) {
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
||||||
// We want to unselect all groups hitElement is part of
|
this.setState((_prevState) => {
|
||||||
// as well as all elements that are part of the groups
|
const nextSelectedElementIds = {
|
||||||
// hitElement is part of
|
..._prevState.selectedElementIds,
|
||||||
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
};
|
||||||
.flatMap((groupId) =>
|
|
||||||
getElementsInGroup(
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
groupId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((element) => ({ [element.id]: false }))
|
|
||||||
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
|
|
||||||
|
|
||||||
this.setState((_prevState) => ({
|
// We want to unselect all groups hitElement is part of
|
||||||
selectedGroupIds: {
|
// as well as all elements that are part of the groups
|
||||||
..._prevState.selectedElementIds,
|
// hitElement is part of
|
||||||
...hitElement.groupIds
|
for (const groupedElement of hitElement.groupIds.flatMap(
|
||||||
.map((gId) => ({ [gId]: false }))
|
(groupId) =>
|
||||||
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
|
getElementsInGroup(
|
||||||
},
|
this.scene.getNonDeletedElements(),
|
||||||
selectedElementIds: {
|
groupId,
|
||||||
..._prevState.selectedElementIds,
|
),
|
||||||
...idsOfSelectedElementsThatAreInGroups,
|
)) {
|
||||||
},
|
delete nextSelectedElementIds[groupedElement.id];
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedGroupIds: {
|
||||||
|
..._prevState.selectedElementIds,
|
||||||
|
...hitElement.groupIds
|
||||||
|
.map((gId) => ({ [gId]: false }))
|
||||||
|
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
|
||||||
|
},
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextSelectedElementIds,
|
||||||
|
_prevState,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
// if not gragging a linear element point (outside editor)
|
// if not gragging a linear element point (outside editor)
|
||||||
} else if (!this.state.selectedLinearElement?.isDragging) {
|
} else if (!this.state.selectedLinearElement?.isDragging) {
|
||||||
// remove element from selection while
|
// remove element from selection while
|
||||||
@@ -6174,11 +6216,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const newSelectedElementIds = {
|
const newSelectedElementIds = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[hitElement!.id]: false,
|
|
||||||
};
|
};
|
||||||
|
delete newSelectedElementIds[hitElement!.id];
|
||||||
const newSelectedElements = getSelectedElements(
|
const newSelectedElements = getSelectedElements(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
{ ...prevState, selectedElementIds: newSelectedElementIds },
|
{ selectedElementIds: newSelectedElementIds },
|
||||||
);
|
);
|
||||||
|
|
||||||
return selectGroupsForSelectedElements(
|
return selectGroupsForSelectedElements(
|
||||||
@@ -6196,6 +6238,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: prevState.selectedLinearElement,
|
: prevState.selectedLinearElement,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6206,21 +6250,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// when hitElement is part of a selected frame, deselect the frame
|
// when hitElement is part of a selected frame, deselect the frame
|
||||||
// to avoid frame and containing elements selected simultaneously
|
// to avoid frame and containing elements selected simultaneously
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const nextSelectedElementIds = {
|
const nextSelectedElementIds: {
|
||||||
|
[id: string]: true;
|
||||||
|
} = {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
[hitElement.id]: true,
|
[hitElement.id]: true,
|
||||||
// deselect the frame
|
|
||||||
[hitElement.frameId!]: false,
|
|
||||||
};
|
};
|
||||||
|
// deselect the frame
|
||||||
|
delete nextSelectedElementIds[hitElement.frameId!];
|
||||||
|
|
||||||
// deselect groups containing the frame
|
// deselect groups containing the frame
|
||||||
(this.scene.getElement(hitElement.frameId!)?.groupIds ?? [])
|
(this.scene.getElement(hitElement.frameId!)?.groupIds ?? [])
|
||||||
.flatMap((gid) =>
|
.flatMap((gid) =>
|
||||||
getElementsInGroup(this.scene.getNonDeletedElements(), gid),
|
getElementsInGroup(this.scene.getNonDeletedElements(), gid),
|
||||||
)
|
)
|
||||||
.forEach(
|
.forEach((element) => {
|
||||||
(element) => (nextSelectedElementIds[element.id] = false),
|
delete nextSelectedElementIds[element.id];
|
||||||
);
|
});
|
||||||
|
|
||||||
return selectGroupsForSelectedElements(
|
return selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
@@ -6229,15 +6275,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
showHyperlinkPopup: hitElement.link ? "info" : false,
|
showHyperlinkPopup: hitElement.link ? "info" : false,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// add element to selection while keeping prev elements selected
|
// add element to selection while keeping prev elements selected
|
||||||
this.setState((_prevState) => ({
|
this.setState((_prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
..._prevState.selectedElementIds,
|
{
|
||||||
[hitElement!.id]: true,
|
..._prevState.selectedElementIds,
|
||||||
},
|
[hitElement!.id]: true,
|
||||||
|
},
|
||||||
|
_prevState,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -6255,6 +6306,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: prevState.selectedLinearElement,
|
: prevState.selectedLinearElement,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -6279,7 +6332,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
} else {
|
} else {
|
||||||
// Deselect selected elements
|
// Deselect selected elements
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
@@ -6290,13 +6343,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
!activeTool.locked &&
|
!activeTool.locked &&
|
||||||
activeTool.type !== "freedraw" &&
|
activeTool.type !== "freedraw" &&
|
||||||
draggingElement
|
draggingElement &&
|
||||||
|
draggingElement.type !== "selection"
|
||||||
) {
|
) {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
...prevState.selectedElementIds,
|
{
|
||||||
[draggingElement.id]: true,
|
...prevState.selectedElementIds,
|
||||||
},
|
[draggingElement.id]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6310,9 +6367,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
|
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
|
||||||
(isBindingEnabled(this.state)
|
(isBindingEnabled(this.state)
|
||||||
? bindOrUnbindSelectedElements
|
? bindOrUnbindSelectedElements
|
||||||
: unbindLinearElements)(
|
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
|
||||||
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||||
@@ -6610,7 +6665,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.initializeImageDimensions(imageElement);
|
this.initializeImageDimensions(imageElement);
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
selectedElementIds: { [imageElement.id]: true },
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{ [imageElement.id]: true },
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -6837,7 +6895,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
// Continue editing the same group if the user selected a different
|
// Continue editing the same group if the user selected a different
|
||||||
// element from it
|
// element from it
|
||||||
@@ -6849,7 +6907,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
previousSelectedElementIds: this.state.selectedElementIds,
|
previousSelectedElementIds: this.state.selectedElementIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6918,7 +6976,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||||
this.insertImageElement(imageElement, file);
|
this.insertImageElement(imageElement, file);
|
||||||
this.initializeImageDimensions(imageElement);
|
this.initializeImageDimensions(imageElement);
|
||||||
this.setState({ selectedElementIds: { [imageElement.id]: true } });
|
this.setState({
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{ [imageElement.id]: true },
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7011,10 +7074,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
includeLockedElements: true,
|
includeLockedElements: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
const isHittignCommonBoundBox =
|
const isHittignCommonBoundBox =
|
||||||
this.isHittingCommonBoundingBoxOfSelectedElements(
|
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||||
{ x, y },
|
{ x, y },
|
||||||
@@ -7043,6 +7103,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.state,
|
||||||
|
this,
|
||||||
)
|
)
|
||||||
: this.state),
|
: this.state),
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
@@ -7130,10 +7192,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
event: MouseEvent | KeyboardEvent,
|
event: MouseEvent | KeyboardEvent,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
const selectedFrames = selectedElements.filter(
|
const selectedFrames = selectedElements.filter(
|
||||||
(element) => element.type === "frame",
|
(element) => element.type === "frame",
|
||||||
) as ExcalidrawFrameElement[];
|
) as ExcalidrawFrameElement[];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { ColorPaletteCustom } from "../../colors";
|
import { ColorPaletteCustom } from "../../colors";
|
||||||
import { t } from "../../i18n";
|
import { TranslationKeys, t } from "../../i18n";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
@@ -48,7 +48,11 @@ const PickerColorList = ({
|
|||||||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||||
|
|
||||||
const keybinding = colorPickerHotkeyBindings[index];
|
const keybinding = colorPickerHotkeyBindings[index];
|
||||||
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
const label = t(
|
||||||
|
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
import { t } from "../i18n";
|
import { t, TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
import "./ContextMenu.scss";
|
import "./ContextMenu.scss";
|
||||||
import {
|
import {
|
||||||
@@ -82,9 +82,15 @@ export const ContextMenu = React.memo(
|
|||||||
let label = "";
|
let label = "";
|
||||||
if (item.contextItemLabel) {
|
if (item.contextItemLabel) {
|
||||||
if (typeof item.contextItemLabel === "function") {
|
if (typeof item.contextItemLabel === "function") {
|
||||||
label = t(item.contextItemLabel(elements, appState));
|
label = t(
|
||||||
|
item.contextItemLabel(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager.app,
|
||||||
|
) as unknown as TranslationKeys,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
label = t(item.contextItemLabel);
|
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { AppClassProperties, Device, UIAppState } from "../types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { Device, UIAppState } from "../types";
|
|
||||||
import {
|
import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -15,17 +13,12 @@ import "./HintViewer.scss";
|
|||||||
|
|
||||||
interface HintViewerProps {
|
interface HintViewerProps {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
app: AppClassProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHints = ({
|
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||||
appState,
|
|
||||||
elements,
|
|
||||||
isMobile,
|
|
||||||
device,
|
|
||||||
}: HintViewerProps) => {
|
|
||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
@@ -55,7 +48,7 @@ const getHints = ({
|
|||||||
return t("hints.placeImage");
|
return t("hints.placeImage");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isResizing &&
|
isResizing &&
|
||||||
@@ -115,15 +108,15 @@ const getHints = ({
|
|||||||
|
|
||||||
export const HintViewer = ({
|
export const HintViewer = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
device,
|
device,
|
||||||
|
app,
|
||||||
}: HintViewerProps) => {
|
}: HintViewerProps) => {
|
||||||
let hint = getHints({
|
let hint = getHints({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
device,
|
device,
|
||||||
|
app,
|
||||||
});
|
});
|
||||||
if (!hint) {
|
if (!hint) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ interface LayerUIProps {
|
|||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
app: AppClassProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@@ -127,6 +128,7 @@ const LayerUI = ({
|
|||||||
onExportImage,
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
|
app,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -240,9 +242,9 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
|
||||||
isMobile={device.isMobile}
|
isMobile={device.isMobile}
|
||||||
device={device}
|
device={device}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
@@ -401,6 +403,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
|
app={app}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
Device,
|
||||||
|
ExcalidrawProps,
|
||||||
|
UIAppState,
|
||||||
|
} from "../types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@@ -41,6 +47,7 @@ type MobileMenuProps = {
|
|||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
|
app: AppClassProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -58,6 +65,7 @@ export const MobileMenu = ({
|
|||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
|
app,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const {
|
const {
|
||||||
WelcomeScreenCenterTunnel,
|
WelcomeScreenCenterTunnel,
|
||||||
@@ -119,9 +127,9 @@ export const MobileMenu = ({
|
|||||||
</Section>
|
</Section>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
device={device}
|
device={device}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { t } from "../i18n";
|
|||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const Section: React.FC<{
|
export const Section: React.FC<{
|
||||||
heading: string;
|
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ heading, children, ...props }) => {
|
}> = ({ heading, children, ...props }) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
|
|||||||
import fallbackLangData from "../locales/en.json";
|
import fallbackLangData from "../locales/en.json";
|
||||||
|
|
||||||
import Trans from "./Trans";
|
import Trans from "./Trans";
|
||||||
|
import { TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
describe("Test <Trans/>", () => {
|
describe("Test <Trans/>", () => {
|
||||||
it("should translate the the strings correctly", () => {
|
it("should translate the the strings correctly", () => {
|
||||||
@@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
|
|||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<>
|
<>
|
||||||
<div data-testid="test1">
|
<div data-testid="test1">
|
||||||
<Trans i18nKey="transTest.key1" audience="world" />
|
<Trans
|
||||||
|
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||||
|
audience="world"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test2">
|
<div data-testid="test2">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key2"
|
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test3">
|
<div data-testid="test3">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key3"
|
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test4">
|
<div data-testid="test4">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key4"
|
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
bold={(el) => <strong>{el}</strong>}
|
bold={(el) => <strong>{el}</strong>}
|
||||||
@@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
|
|||||||
</div>
|
</div>
|
||||||
<div data-testid="test5">
|
<div data-testid="test5">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key5"
|
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useI18n } from "../i18n";
|
import { TranslationKeys, useI18n } from "../i18n";
|
||||||
|
|
||||||
// Used for splitting i18nKey into tokens in Trans component
|
// Used for splitting i18nKey into tokens in Trans component
|
||||||
// Example:
|
// Example:
|
||||||
@@ -153,7 +153,7 @@ const Trans = ({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
i18nKey: string;
|
i18nKey: TranslationKeys;
|
||||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
debug: typeof Debug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lessPrecise = (num: number, precision = 5) =>
|
||||||
|
parseFloat(num.toPrecision(precision));
|
||||||
|
|
||||||
|
const getAvgFrameTime = (times: number[]) =>
|
||||||
|
lessPrecise(times.reduce((a, b) => a + b) / times.length);
|
||||||
|
|
||||||
|
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
|
||||||
|
|
||||||
|
export class Debug {
|
||||||
|
public static DEBUG_LOG_TIMES = true;
|
||||||
|
|
||||||
|
private static TIMES_AGGR: Record<string, { t: number; times: number[] }> =
|
||||||
|
{};
|
||||||
|
private static TIMES_AVG: Record<
|
||||||
|
string,
|
||||||
|
{ t: number; times: number[]; avg: number | null }
|
||||||
|
> = {};
|
||||||
|
private static LAST_DEBUG_LOG_CALL = 0;
|
||||||
|
private static DEBUG_LOG_INTERVAL_ID: null | number = null;
|
||||||
|
|
||||||
|
private static setupInterval = () => {
|
||||||
|
if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
|
||||||
|
console.info("%c(starting perf recording)", "color: lime");
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
|
||||||
|
}
|
||||||
|
Debug.LAST_DEBUG_LOG_CALL = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
private static debugLogger = () => {
|
||||||
|
if (
|
||||||
|
Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID !== null
|
||||||
|
) {
|
||||||
|
window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID = null;
|
||||||
|
for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) {
|
||||||
|
if (avg != null) {
|
||||||
|
console.info(
|
||||||
|
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
|
||||||
|
"color: blue",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info("%c(stopping perf recording)", "color: red");
|
||||||
|
Debug.TIMES_AGGR = {};
|
||||||
|
Debug.TIMES_AVG = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Debug.DEBUG_LOG_TIMES) {
|
||||||
|
for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
|
||||||
|
if (times.length) {
|
||||||
|
console.info(
|
||||||
|
name,
|
||||||
|
lessPrecise(times.reduce((a, b) => a + b)),
|
||||||
|
times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
|
||||||
|
);
|
||||||
|
Debug.TIMES_AGGR[name] = { t, times: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
|
||||||
|
if (times.length) {
|
||||||
|
const avgFrameTime = getAvgFrameTime(times);
|
||||||
|
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
|
||||||
|
Debug.TIMES_AVG[name] = {
|
||||||
|
t,
|
||||||
|
times: [],
|
||||||
|
avg:
|
||||||
|
avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static logTime = (time?: number, name = "default") => {
|
||||||
|
Debug.setupInterval();
|
||||||
|
const now = performance.now();
|
||||||
|
const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || {
|
||||||
|
t: 0,
|
||||||
|
times: [],
|
||||||
|
});
|
||||||
|
if (t) {
|
||||||
|
times.push(time != null ? time : now - t);
|
||||||
|
}
|
||||||
|
Debug.TIMES_AGGR[name].t = now;
|
||||||
|
};
|
||||||
|
public static logTimeAverage = (time?: number, name = "default") => {
|
||||||
|
Debug.setupInterval();
|
||||||
|
const now = performance.now();
|
||||||
|
const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || {
|
||||||
|
t: 0,
|
||||||
|
times: [],
|
||||||
|
});
|
||||||
|
if (t) {
|
||||||
|
times.push(time != null ? time : now - t);
|
||||||
|
}
|
||||||
|
Debug.TIMES_AVG[name].t = now;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static logWrapper =
|
||||||
|
(type: "logTime" | "logTimeAverage") =>
|
||||||
|
<T extends any[], R>(fn: (...args: T) => R, name = "default") => {
|
||||||
|
return (...args: T) => {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const ret = fn(...args);
|
||||||
|
Debug.logTime(performance.now() - t0, name);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public static logTimeWrap = Debug.logWrapper("logTime");
|
||||||
|
public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage");
|
||||||
|
|
||||||
|
public static perfWrap = <T extends any[], R>(
|
||||||
|
fn: (...args: T) => R,
|
||||||
|
name = "default",
|
||||||
|
) => {
|
||||||
|
return (...args: T) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.time(name);
|
||||||
|
const ret = fn(...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.timeEnd(name);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.debug = Debug;
|
||||||
+7
-2
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "./element/textElement";
|
} from "./element/textElement";
|
||||||
import { arrayToMap, findIndex } from "./utils";
|
import { arrayToMap, findIndex } from "./utils";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { AppState } from "./types";
|
import { AppClassProperties, AppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
import { moveOneRight } from "./zindex";
|
||||||
@@ -571,8 +571,13 @@ export const replaceAllElementsInFrame = (
|
|||||||
export const updateFrameMembershipOfSelectedElements = (
|
export const updateFrameMembershipOfSelectedElements = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(allElements, appState);
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
// supplying elements explicitly in case we're passed non-state elements
|
||||||
|
elements: allElements,
|
||||||
|
});
|
||||||
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
|
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
|
||||||
|
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
|
|||||||
+35
-5
@@ -1,7 +1,13 @@
|
|||||||
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
import {
|
||||||
import { AppState } from "./types";
|
GroupId,
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "./element/types";
|
||||||
|
import { AppClassProperties, AppState } from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
|
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
@@ -66,14 +72,33 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
|||||||
*/
|
*/
|
||||||
export const selectGroupsForSelectedElements = (
|
export const selectGroupsForSelectedElements = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
prevAppState: AppState,
|
||||||
|
/**
|
||||||
|
* supply null in cases where you don't have access to App instance and
|
||||||
|
* you don't care about optimizing selectElements retrieval
|
||||||
|
*/
|
||||||
|
app: AppClassProperties | null,
|
||||||
): AppState => {
|
): AppState => {
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app
|
||||||
|
? app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
// supplying elements explicitly in case we're passed non-state elements
|
||||||
|
elements,
|
||||||
|
})
|
||||||
|
: getSelectedElements(elements, appState);
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
if (!selectedElements.length) {
|
||||||
return { ...nextAppState, editingGroupId: null };
|
return {
|
||||||
|
...nextAppState,
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextAppState.selectedElementIds,
|
||||||
|
prevAppState,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
for (const selectedElement of selectedElements) {
|
||||||
@@ -91,6 +116,11 @@ export const selectGroupsForSelectedElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextAppState.selectedElementIds = makeNextSelectedElementIds(
|
||||||
|
nextAppState.selectedElementIds,
|
||||||
|
prevAppState,
|
||||||
|
);
|
||||||
|
|
||||||
return nextAppState;
|
return nextAppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,7 @@ import percentages from "./locales/percentages.json";
|
|||||||
import { ENV } from "./constants";
|
import { ENV } from "./constants";
|
||||||
import { jotaiScope, jotaiStore } from "./jotai";
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
import { NestedKeyOf } from "./utility-types";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ export interface Language {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||||
|
|
||||||
export const defaultLang = { code: "en", label: "English" };
|
export const defaultLang = { code: "en", label: "English" };
|
||||||
|
|
||||||
export const languages: Language[] = [
|
export const languages: Language[] = [
|
||||||
@@ -123,7 +126,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path: string,
|
path: NestedKeyOf<typeof fallbackLangData>,
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "dist/excalidraw.production.min.js",
|
||||||
|
"limit": "285 kB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "dist/excalidraw-assets/locales",
|
||||||
|
"name": "dist/excalidraw-assets/locales",
|
||||||
|
"limit": "270 kB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "dist/excalidraw-assets/vendor-*.js",
|
||||||
|
"name": "dist/excalidraw-assets/vendor*.js",
|
||||||
|
"limit": "30 kB"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"@babel/preset-env": "7.18.6",
|
"@babel/preset-env": "7.18.6",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.18.6",
|
||||||
"@babel/preset-typescript": "7.18.6",
|
"@babel/preset-typescript": "7.18.6",
|
||||||
|
"@size-limit/preset-big-lib": "8.2.6",
|
||||||
"autoprefixer": "10.4.7",
|
"autoprefixer": "10.4.7",
|
||||||
"babel-loader": "8.2.5",
|
"babel-loader": "8.2.5",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
@@ -61,6 +62,8 @@
|
|||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
|
"size-limit": "8.2.4",
|
||||||
|
"style-loader": "3.3.3",
|
||||||
"terser-webpack-plugin": "5.3.3",
|
"terser-webpack-plugin": "5.3.3",
|
||||||
"ts-loader": "9.3.1",
|
"ts-loader": "9.3.1",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
"pack": "yarn build:umd && yarn pack",
|
"pack": "yarn build:umd && yarn pack",
|
||||||
"start": "webpack serve --config webpack.dev-server.config.js",
|
"start": "webpack serve --config webpack.dev-server.config.js",
|
||||||
"install:deps": "yarn install --frozen-lockfile && yarn --cwd ../../../",
|
"install:deps": "yarn install --frozen-lockfile && yarn --cwd ../../../",
|
||||||
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types"
|
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types",
|
||||||
|
"size": "yarn build:umd && size-limit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -931,7 +931,11 @@ export const renderElement = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "frame": {
|
case "frame": {
|
||||||
if (!renderConfig.isExporting && appState.shouldRenderFrames) {
|
if (
|
||||||
|
!renderConfig.isExporting &&
|
||||||
|
appState.frameRendering.enabled &&
|
||||||
|
appState.frameRendering.outline
|
||||||
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(
|
context.translate(
|
||||||
element.x + renderConfig.scrollX,
|
element.x + renderConfig.scrollX,
|
||||||
|
|||||||
@@ -470,7 +470,9 @@ export const _renderScene = ({
|
|||||||
if (
|
if (
|
||||||
frameId &&
|
frameId &&
|
||||||
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
|
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
|
||||||
(!renderConfig.isExporting && appState.shouldRenderFrames))
|
(!renderConfig.isExporting &&
|
||||||
|
appState.frameRendering.enabled &&
|
||||||
|
appState.frameRendering.clip))
|
||||||
) {
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import {
|
|||||||
} from "../element";
|
} from "../element";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isFrameElement } from "../element/typeChecks";
|
import { isFrameElement } from "../element/typeChecks";
|
||||||
|
import { getSelectedElements } from "./selection";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { Assert, SameType } from "../utility-types";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@@ -18,6 +21,31 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
|
|||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
|
|
||||||
|
type SelectionHash = string & { __brand: "selectionHash" };
|
||||||
|
|
||||||
|
const hashSelectionOpts = (
|
||||||
|
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
||||||
|
) => {
|
||||||
|
const keys = ["includeBoundTextElement", "includeElementsInFrames"] as const;
|
||||||
|
|
||||||
|
type HashableKeys = Omit<typeof opts, "selectedElementIds" | "elements">;
|
||||||
|
|
||||||
|
// just to ensure we're hashing all expected keys
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
type _ = Assert<
|
||||||
|
SameType<
|
||||||
|
Required<HashableKeys>,
|
||||||
|
Pick<Required<HashableKeys>, typeof keys[number]>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
let hash = "";
|
||||||
|
for (const key of keys) {
|
||||||
|
hash += `${key}:${opts[key] ? "1" : "0"}`;
|
||||||
|
}
|
||||||
|
return hash as SelectionHash;
|
||||||
|
};
|
||||||
|
|
||||||
// ideally this would be a branded type but it'd be insanely hard to work with
|
// ideally this would be a branded type but it'd be insanely hard to work with
|
||||||
// in our codebase
|
// in our codebase
|
||||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||||
@@ -68,6 +96,15 @@ class Scene {
|
|||||||
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
|
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
|
||||||
private frames: readonly ExcalidrawFrameElement[] = [];
|
private frames: readonly ExcalidrawFrameElement[] = [];
|
||||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
||||||
|
private selectedElementsCache: {
|
||||||
|
selectedElementIds: AppState["selectedElementIds"] | null;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[] | null;
|
||||||
|
cache: Map<SelectionHash, NonDeletedExcalidrawElement[]>;
|
||||||
|
} = {
|
||||||
|
selectedElementIds: null,
|
||||||
|
elements: null,
|
||||||
|
cache: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
getElementsIncludingDeleted() {
|
getElementsIncludingDeleted() {
|
||||||
return this.elements;
|
return this.elements;
|
||||||
@@ -81,6 +118,52 @@ class Scene {
|
|||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectedElements(opts: {
|
||||||
|
// NOTE can be ommitted by making Scene constructor require App instance
|
||||||
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
|
/**
|
||||||
|
* for specific cases where you need to use elements not from current
|
||||||
|
* scene state. This in effect will likely result in cache-miss, and
|
||||||
|
* the cache won't be updated in this case.
|
||||||
|
*/
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
// selection-related options
|
||||||
|
includeBoundTextElement?: boolean;
|
||||||
|
includeElementsInFrames?: boolean;
|
||||||
|
}): NonDeleted<ExcalidrawElement>[] {
|
||||||
|
const hash = hashSelectionOpts(opts);
|
||||||
|
|
||||||
|
const elements = opts?.elements || this.nonDeletedElements;
|
||||||
|
if (
|
||||||
|
this.selectedElementsCache.elements === elements &&
|
||||||
|
this.selectedElementsCache.selectedElementIds === opts.selectedElementIds
|
||||||
|
) {
|
||||||
|
const cached = this.selectedElementsCache.cache.get(hash);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
} else if (opts?.elements == null) {
|
||||||
|
// if we're operating on latest scene elements and the cache is not
|
||||||
|
// storing the latest elements, clear the cache
|
||||||
|
this.selectedElementsCache.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
elements,
|
||||||
|
{ selectedElementIds: opts.selectedElementIds },
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
|
||||||
|
// cache only if we're not using custom elements
|
||||||
|
if (opts?.elements == null) {
|
||||||
|
this.selectedElementsCache.selectedElementIds = opts.selectedElementIds;
|
||||||
|
this.selectedElementsCache.elements = this.nonDeletedElements;
|
||||||
|
this.selectedElementsCache.cache.set(hash, selectedElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedElements;
|
||||||
|
}
|
||||||
|
|
||||||
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
|
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
|
||||||
return this.nonDeletedFrames;
|
return this.nonDeletedFrames;
|
||||||
}
|
}
|
||||||
@@ -168,11 +251,21 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.nonDeletedElements = [];
|
||||||
|
this.elements = [];
|
||||||
|
this.nonDeletedFrames = [];
|
||||||
|
this.frames = [];
|
||||||
|
this.elementsMap.clear();
|
||||||
|
this.selectedElementsCache.selectedElementIds = null;
|
||||||
|
this.selectedElementsCache.elements = null;
|
||||||
|
this.selectedElementsCache.cache.clear();
|
||||||
|
|
||||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
Scene.sceneMapById.forEach((scene, elementKey) => {
|
||||||
if (scene === this) {
|
if (scene === this) {
|
||||||
Scene.sceneMapById.delete(elementKey);
|
Scene.sceneMapById.delete(elementKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { makeNextSelectedElementIds } from "./selection";
|
||||||
|
|
||||||
|
describe("makeNextSelectedElementIds", () => {
|
||||||
|
const _makeNextSelectedElementIds = (
|
||||||
|
selectedElementIds: { [id: string]: true },
|
||||||
|
prevSelectedElementIds: { [id: string]: true },
|
||||||
|
expectUpdated: boolean,
|
||||||
|
) => {
|
||||||
|
const ret = makeNextSelectedElementIds(selectedElementIds, {
|
||||||
|
selectedElementIds: prevSelectedElementIds,
|
||||||
|
});
|
||||||
|
expect(ret === selectedElementIds).toBe(expectUpdated);
|
||||||
|
};
|
||||||
|
it("should return prevState selectedElementIds if no change", () => {
|
||||||
|
_makeNextSelectedElementIds({}, {}, false);
|
||||||
|
_makeNextSelectedElementIds({ 1: true }, { 1: true }, false);
|
||||||
|
_makeNextSelectedElementIds(
|
||||||
|
{ 1: true, 2: true },
|
||||||
|
{ 1: true, 2: true },
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("should return new selectedElementIds if changed", () => {
|
||||||
|
// _makeNextSelectedElementIds({ 1: true }, { 1: false }, true);
|
||||||
|
_makeNextSelectedElementIds({ 1: true }, {}, true);
|
||||||
|
_makeNextSelectedElementIds({}, { 1: true }, true);
|
||||||
|
_makeNextSelectedElementIds({ 1: true }, { 2: true }, true);
|
||||||
|
_makeNextSelectedElementIds({ 1: true }, { 1: true, 2: true }, true);
|
||||||
|
_makeNextSelectedElementIds(
|
||||||
|
{ 1: true, 2: true },
|
||||||
|
{ 1: true, 3: true },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+51
-5
@@ -10,6 +10,7 @@ import {
|
|||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameElements,
|
getFrameElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
|
import { isShallowEqual } from "../utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames and their containing elements are not to be selected at the same time.
|
* Frames and their containing elements are not to be selected at the same time.
|
||||||
@@ -88,11 +89,41 @@ export const getElementsWithinSelection = (
|
|||||||
return elementsInSelection;
|
return elementsInSelection;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isSomeElementSelected = (
|
// FIXME move this into the editor instance to keep utility methods stateless
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
export const isSomeElementSelected = (function () {
|
||||||
appState: Pick<AppState, "selectedElementIds">,
|
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
|
||||||
): boolean =>
|
let lastSelectedElementIds: AppState["selectedElementIds"] | null = null;
|
||||||
elements.some((element) => appState.selectedElementIds[element.id]);
|
let isSelected: boolean | null = null;
|
||||||
|
|
||||||
|
const ret = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: Pick<AppState, "selectedElementIds">,
|
||||||
|
): boolean => {
|
||||||
|
if (
|
||||||
|
isSelected != null &&
|
||||||
|
elements === lastElements &&
|
||||||
|
appState.selectedElementIds === lastSelectedElementIds
|
||||||
|
) {
|
||||||
|
return isSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected = elements.some(
|
||||||
|
(element) => appState.selectedElementIds[element.id],
|
||||||
|
);
|
||||||
|
lastElements = elements;
|
||||||
|
lastSelectedElementIds = appState.selectedElementIds;
|
||||||
|
|
||||||
|
return isSelected;
|
||||||
|
};
|
||||||
|
|
||||||
|
ret.clearCache = () => {
|
||||||
|
lastElements = null;
|
||||||
|
lastSelectedElementIds = null;
|
||||||
|
isSelected = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns common attribute (picked by `getAttribute` callback) of selected
|
* Returns common attribute (picked by `getAttribute` callback) of selected
|
||||||
@@ -161,3 +192,18 @@ export const getTargetElements = (
|
|||||||
: getSelectedElements(elements, appState, {
|
: getSelectedElements(elements, appState, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns prevState's selectedElementids if no change from previous, so as to
|
||||||
|
* retain reference identity for memoization
|
||||||
|
*/
|
||||||
|
export const makeNextSelectedElementIds = (
|
||||||
|
nextSelectedElementIds: AppState["selectedElementIds"],
|
||||||
|
prevState: Pick<AppState, "selectedElementIds">,
|
||||||
|
) => {
|
||||||
|
if (isShallowEqual(prevState.selectedElementIds, nextSelectedElementIds)) {
|
||||||
|
return prevState.selectedElementIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextSelectedElementIds;
|
||||||
|
};
|
||||||
|
|||||||
@@ -314,6 +314,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -353,7 +359,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -501,6 +506,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -537,7 +548,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -694,6 +704,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -730,7 +746,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -1061,6 +1076,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -1097,7 +1118,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -1428,6 +1448,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -1464,7 +1490,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -1502,14 +1527,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -1561,14 +1586,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -1621,6 +1646,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -1655,7 +1686,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -1851,6 +1881,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -1887,7 +1923,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -2146,6 +2181,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -2179,7 +2220,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": Object {
|
"selectedGroupIds": Object {
|
||||||
@@ -2188,7 +2228,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -2413,7 +2452,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"selectedGroupIds": Object {
|
"selectedGroupIds": Object {
|
||||||
"id3": true,
|
"id3": true,
|
||||||
@@ -2531,6 +2569,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -2567,7 +2611,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -3404,6 +3447,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -3440,7 +3489,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -3771,6 +3819,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -3807,7 +3861,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -4138,6 +4191,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -4171,14 +4230,12 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": Object {},
|
"selectedGroupIds": Object {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -4214,14 +4271,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 238820263,
|
"versionNonce": 1014066025,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4246,14 +4303,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 238820263,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -4305,14 +4362,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4348,14 +4405,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4377,14 +4434,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -4399,7 +4456,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"selectedGroupIds": Object {
|
"selectedGroupIds": Object {
|
||||||
"id3": true,
|
"id3": true,
|
||||||
@@ -4426,14 +4482,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1116226695,
|
"versionNonce": 1150084233,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4457,14 +4513,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1116226695,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -4479,7 +4535,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"selectedGroupIds": Object {},
|
"selectedGroupIds": Object {},
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
@@ -4502,14 +4557,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 238820263,
|
"versionNonce": 1014066025,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4531,14 +4586,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 238820263,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -4867,6 +4922,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -4892,7 +4953,6 @@ Object {
|
|||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
@@ -4901,15 +4961,12 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
"id3": true,
|
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": Object {},
|
"selectedGroupIds": Object {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -5444,6 +5501,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -5469,7 +5532,6 @@ Object {
|
|||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
@@ -5478,8 +5540,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
"id3": true,
|
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": Object {
|
"selectedGroupIds": Object {
|
||||||
@@ -5488,7 +5548,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -5526,14 +5585,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1116226695,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5560,14 +5619,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 238820263,
|
"versionNonce": 1014066025,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5619,14 +5678,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5662,14 +5721,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 449462985,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5691,14 +5750,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5713,8 +5772,6 @@ Object {
|
|||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
|
||||||
"id3": true,
|
|
||||||
},
|
},
|
||||||
"selectedGroupIds": Object {
|
"selectedGroupIds": Object {
|
||||||
"id4": true,
|
"id4": true,
|
||||||
@@ -5741,14 +5798,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1116226695,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5772,14 +5829,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 238820263,
|
"versionNonce": 1014066025,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5947,6 +6004,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -5981,7 +6044,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -6337,6 +6399,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -6373,7 +6441,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
@@ -6705,6 +6772,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
@@ -6741,7 +6814,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+32
-8
@@ -430,7 +430,10 @@ describe("arrow", () => {
|
|||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = (7 * Math.PI) / 4;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||||
h.app.scene.replaceAllElements([line]);
|
h.app.scene.replaceAllElements([line]);
|
||||||
h.app.state.selectedElementIds[line.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[line.id]: true,
|
||||||
|
};
|
||||||
mutateElement(line, {
|
mutateElement(line, {
|
||||||
angle: originalAngle,
|
angle: originalAngle,
|
||||||
});
|
});
|
||||||
@@ -446,7 +449,10 @@ describe("arrow", () => {
|
|||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = (7 * Math.PI) / 4;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||||
h.app.scene.replaceAllElements([line]);
|
h.app.scene.replaceAllElements([line]);
|
||||||
h.app.state.selectedElementIds[line.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[line.id]: true,
|
||||||
|
};
|
||||||
mutateElement(line, {
|
mutateElement(line, {
|
||||||
angle: originalAngle,
|
angle: originalAngle,
|
||||||
});
|
});
|
||||||
@@ -616,7 +622,10 @@ describe("line", () => {
|
|||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = (7 * Math.PI) / 4;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||||
h.app.scene.replaceAllElements([line]);
|
h.app.scene.replaceAllElements([line]);
|
||||||
h.app.state.selectedElementIds[line.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[line.id]: true,
|
||||||
|
};
|
||||||
mutateElement(line, {
|
mutateElement(line, {
|
||||||
angle: originalAngle,
|
angle: originalAngle,
|
||||||
});
|
});
|
||||||
@@ -632,7 +641,10 @@ describe("line", () => {
|
|||||||
const expectedAngle = (7 * Math.PI) / 4;
|
const expectedAngle = (7 * Math.PI) / 4;
|
||||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||||
h.app.scene.replaceAllElements([line]);
|
h.app.scene.replaceAllElements([line]);
|
||||||
h.app.state.selectedElementIds[line.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[line.id]: true,
|
||||||
|
};
|
||||||
mutateElement(line, {
|
mutateElement(line, {
|
||||||
angle: originalAngle,
|
angle: originalAngle,
|
||||||
});
|
});
|
||||||
@@ -659,14 +671,20 @@ describe("freedraw", () => {
|
|||||||
it("flips an unrotated drawing horizontally correctly", async () => {
|
it("flips an unrotated drawing horizontally correctly", async () => {
|
||||||
const draw = createAndReturnOneDraw();
|
const draw = createAndReturnOneDraw();
|
||||||
// select draw, since not done automatically
|
// select draw, since not done automatically
|
||||||
h.state.selectedElementIds[draw.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[draw.id]: true,
|
||||||
|
};
|
||||||
await checkHorizontalFlip();
|
await checkHorizontalFlip();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flips an unrotated drawing vertically correctly", async () => {
|
it("flips an unrotated drawing vertically correctly", async () => {
|
||||||
const draw = createAndReturnOneDraw();
|
const draw = createAndReturnOneDraw();
|
||||||
// select draw, since not done automatically
|
// select draw, since not done automatically
|
||||||
h.state.selectedElementIds[draw.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[draw.id]: true,
|
||||||
|
};
|
||||||
await checkVerticalFlip();
|
await checkVerticalFlip();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -676,7 +694,10 @@ describe("freedraw", () => {
|
|||||||
|
|
||||||
const draw = createAndReturnOneDraw(originalAngle);
|
const draw = createAndReturnOneDraw(originalAngle);
|
||||||
// select draw, since not done automatically
|
// select draw, since not done automatically
|
||||||
h.state.selectedElementIds[draw.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[draw.id]: true,
|
||||||
|
};
|
||||||
|
|
||||||
await checkRotatedHorizontalFlip(expectedAngle);
|
await checkRotatedHorizontalFlip(expectedAngle);
|
||||||
});
|
});
|
||||||
@@ -687,7 +708,10 @@ describe("freedraw", () => {
|
|||||||
|
|
||||||
const draw = createAndReturnOneDraw(originalAngle);
|
const draw = createAndReturnOneDraw(originalAngle);
|
||||||
// select draw, since not done automatically
|
// select draw, since not done automatically
|
||||||
h.state.selectedElementIds[draw.id] = true;
|
h.state.selectedElementIds = {
|
||||||
|
...h.state.selectedElementIds,
|
||||||
|
[draw.id]: true,
|
||||||
|
};
|
||||||
|
|
||||||
await checkRotatedVerticalFlip(expectedAngle);
|
await checkRotatedVerticalFlip(expectedAngle);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ Object {
|
|||||||
"exportScale": 1,
|
"exportScale": 1,
|
||||||
"exportWithDarkMode": false,
|
"exportWithDarkMode": false,
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
|
"frameRendering": Object {
|
||||||
|
"clip": true,
|
||||||
|
"enabled": true,
|
||||||
|
"name": true,
|
||||||
|
"outline": true,
|
||||||
|
},
|
||||||
"frameToHighlight": null,
|
"frameToHighlight": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
@@ -70,7 +76,6 @@ Object {
|
|||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
"shouldRenderFrames": true,
|
|
||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ const populateElements = (
|
|||||||
...selectGroupsForSelectedElements(
|
...selectGroupsForSelectedElements(
|
||||||
{ ...h.state, ...appState, selectedElementIds },
|
{ ...h.state, ...appState, selectedElementIds },
|
||||||
h.elements,
|
h.elements,
|
||||||
|
h.state,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
|
|||||||
+9
-4
@@ -115,7 +115,12 @@ export type AppState = {
|
|||||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
suggestedBindings: SuggestedBinding[];
|
suggestedBindings: SuggestedBinding[];
|
||||||
frameToHighlight: NonDeleted<ExcalidrawFrameElement> | null;
|
frameToHighlight: NonDeleted<ExcalidrawFrameElement> | null;
|
||||||
shouldRenderFrames: boolean;
|
frameRendering: {
|
||||||
|
enabled: boolean;
|
||||||
|
name: boolean;
|
||||||
|
outline: boolean;
|
||||||
|
clip: boolean;
|
||||||
|
};
|
||||||
editingFrame: string | null;
|
editingFrame: string | null;
|
||||||
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
|
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
|
||||||
// element being edited, but not necessarily added to elements array yet
|
// element being edited, but not necessarily added to elements array yet
|
||||||
@@ -181,8 +186,8 @@ export type AppState = {
|
|||||||
defaultSidebarDockedPreference: boolean;
|
defaultSidebarDockedPreference: boolean;
|
||||||
|
|
||||||
lastPointerDownWith: PointerType;
|
lastPointerDownWith: PointerType;
|
||||||
selectedElementIds: { [id: string]: boolean };
|
selectedElementIds: Readonly<{ [id: string]: true }>;
|
||||||
previousSelectedElementIds: { [id: string]: boolean };
|
previousSelectedElementIds: { [id: string]: true };
|
||||||
selectedElementsAreBeingDragged: boolean;
|
selectedElementsAreBeingDragged: boolean;
|
||||||
shouldCacheIgnoreZoom: boolean;
|
shouldCacheIgnoreZoom: boolean;
|
||||||
toast: { message: string; closable?: boolean; duration?: number } | null;
|
toast: { message: string; closable?: boolean; duration?: number } | null;
|
||||||
@@ -543,7 +548,7 @@ export type ExcalidrawImperativeAPI = {
|
|||||||
* the frames are still interactive in edit mode. As such, this API should be
|
* the frames are still interactive in edit mode. As such, this API should be
|
||||||
* used in conjunction with view mode (props.viewModeEnabled).
|
* used in conjunction with view mode (props.viewModeEnabled).
|
||||||
*/
|
*/
|
||||||
toggleFrameRendering: InstanceType<typeof App>["toggleFrameRendering"];
|
updateFrameRendering: InstanceType<typeof App>["updateFrameRendering"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Device = Readonly<{
|
export type Device = Readonly<{
|
||||||
|
|||||||
@@ -47,3 +47,10 @@ export type ForwardRef<T, P = any> = Parameters<
|
|||||||
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
||||||
? U
|
? U
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
|
||||||
|
export type Assert<T extends true> = T;
|
||||||
|
|
||||||
|
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
|
||||||
|
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
|
||||||
|
: never;
|
||||||
|
|||||||
@@ -10267,7 +10267,7 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5:
|
|||||||
serialize-javascript "^6.0.1"
|
serialize-javascript "^6.0.1"
|
||||||
terser "^5.16.5"
|
terser "^5.16.5"
|
||||||
|
|
||||||
terser@^5.0.0, terser@^5.10.0, terser@^5.16.5:
|
terser@^5.0.0, terser@^5.10.0:
|
||||||
version "5.16.9"
|
version "5.16.9"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.9.tgz#7a28cb178e330c484369886f2afd623d9847495f"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.9.tgz#7a28cb178e330c484369886f2afd623d9847495f"
|
||||||
integrity sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==
|
integrity sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==
|
||||||
@@ -10277,6 +10277,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.16.5:
|
|||||||
commander "^2.20.0"
|
commander "^2.20.0"
|
||||||
source-map-support "~0.5.20"
|
source-map-support "~0.5.20"
|
||||||
|
|
||||||
|
terser@^5.16.5:
|
||||||
|
version "5.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69"
|
||||||
|
integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/source-map" "^0.3.2"
|
||||||
|
acorn "^8.5.0"
|
||||||
|
commander "^2.20.0"
|
||||||
|
source-map-support "~0.5.20"
|
||||||
|
|
||||||
test-exclude@^6.0.0:
|
test-exclude@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
||||||
|
|||||||
Reference in New Issue
Block a user