mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Compare commits
18 Commits
barnabasmo
...
arnost/col
Author | SHA1 | Date | |
---|---|---|---|
bef0f3e51d | |||
28b0095c8a | |||
c4ff0594e3 | |||
4608e809b1 | |||
db5149ab5d | |||
2bdf09153c | |||
62df03d78d | |||
a1d3350131 | |||
a2d371bf1d | |||
e340103250 | |||
0567af1bcb | |||
2ffeff442a | |||
ef190ebf30 | |||
e1ff9791f2 | |||
aa91af8f7d | |||
52254bca7c | |||
addf9d71fa | |||
1badf14a93 |
30
.github/workflows/size-limit.yml
vendored
30
.github/workflows/size-limit.yml
vendored
@ -1,30 +0,0 @@
|
||||
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,35 +165,3 @@ function App() {
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | :-: | :-: | --- |
|
||||
| `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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
@ -54,7 +54,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"socket.io-client": "4.6.1",
|
||||
"tunnel-rat": "0.1.2",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
"workbox-broadcast-update": "^6.5.4",
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { register } from "./register";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { randomId } from "../random";
|
||||
import { t } from "../i18n";
|
||||
@ -7,11 +9,14 @@ export const actionAddToLibrary = register({
|
||||
name: "addToLibrary",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
},
|
||||
);
|
||||
if (selectedElements.some((element) => element.type === "image")) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
|
@ -13,18 +13,19 @@ import { ExcalidrawElement } from "../element/types";
|
||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||
import { t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { arrayToMap, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
const alignActionsPredicate = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
_: unknown,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return (
|
||||
selectedElements.length > 1 &&
|
||||
// TODO enable aligning frames when implemented properly
|
||||
@ -35,10 +36,12 @@ const alignActionsPredicate = (
|
||||
const alignSelectedElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
app: AppClassProperties,
|
||||
alignment: Alignment,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
|
||||
const updatedElements = alignElements(selectedElements, alignment);
|
||||
|
||||
@ -47,7 +50,6 @@ const alignSelectedElements = (
|
||||
return updateFrameMembershipOfSelectedElements(
|
||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||
appState,
|
||||
app,
|
||||
);
|
||||
};
|
||||
|
||||
@ -55,10 +57,10 @@ export const actionAlignTop = register({
|
||||
name: "alignTop",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "start",
|
||||
axis: "y",
|
||||
}),
|
||||
@ -67,9 +69,9 @@ export const actionAlignTop = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={AlignTopIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -86,10 +88,10 @@ export const actionAlignBottom = register({
|
||||
name: "alignBottom",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "end",
|
||||
axis: "y",
|
||||
}),
|
||||
@ -98,9 +100,9 @@ export const actionAlignBottom = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={AlignBottomIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -117,10 +119,10 @@ export const actionAlignLeft = register({
|
||||
name: "alignLeft",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "start",
|
||||
axis: "x",
|
||||
}),
|
||||
@ -129,9 +131,9 @@ export const actionAlignLeft = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={AlignLeftIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -148,10 +150,10 @@ export const actionAlignRight = register({
|
||||
name: "alignRight",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "end",
|
||||
axis: "x",
|
||||
}),
|
||||
@ -160,9 +162,9 @@ export const actionAlignRight = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={AlignRightIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -179,19 +181,19 @@ export const actionAlignVerticallyCentered = register({
|
||||
name: "alignVerticallyCentered",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "center",
|
||||
axis: "y",
|
||||
}),
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={CenterVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -206,19 +208,19 @@ export const actionAlignHorizontallyCentered = register({
|
||||
name: "alignHorizontallyCentered",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: alignActionsPredicate,
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: alignSelectedElements(elements, appState, app, {
|
||||
elements: alignSelectedElements(elements, appState, {
|
||||
position: "center",
|
||||
axis: "x",
|
||||
}),
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||
hidden={!alignActionsPredicate(elements, appState)}
|
||||
type="button"
|
||||
icon={CenterHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
VERTICAL_ALIGN,
|
||||
TEXT_ALIGN,
|
||||
} from "../constants";
|
||||
import { isTextElement, newElement } from "../element";
|
||||
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import {
|
||||
computeBoundTextPosition,
|
||||
@ -29,6 +29,7 @@ import {
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawTextElement,
|
||||
} from "../element/types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { getFontString } from "../utils";
|
||||
@ -38,13 +39,16 @@ export const actionUnbindText = register({
|
||||
name: "unbindText",
|
||||
contextItemLabel: "labels.unbindText",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
@ -89,8 +93,8 @@ export const actionBindText = register({
|
||||
name: "bindText",
|
||||
contextItemLabel: "labels.bindText",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
if (selectedElements.length === 2) {
|
||||
const textElement =
|
||||
@ -113,8 +117,11 @@ export const actionBindText = register({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
|
||||
let textElement: ExcalidrawTextElement;
|
||||
let container: ExcalidrawTextContainer;
|
||||
@ -194,13 +201,16 @@ export const actionWrapTextInContainer = register({
|
||||
name: "wrapTextInContainer",
|
||||
contextItemLabel: "labels.createContainerFromText",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||
return selectedElements.length > 0 && areTextElements;
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getNormalizedZoom } from "../scene";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
@ -302,8 +302,11 @@ export const zoomToFit = ({
|
||||
export const actionZoomToFitSelectionInViewport = register({
|
||||
name: "zoomToFitSelectionInViewport",
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return zoomToFit({
|
||||
targetElements: selectedElements.length ? selectedElements : elements,
|
||||
appState,
|
||||
@ -322,8 +325,11 @@ export const actionZoomToFitSelectionInViewport = register({
|
||||
export const actionZoomToFitSelection = register({
|
||||
name: "zoomToFitSelection",
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return zoomToFit({
|
||||
targetElements: selectedElements.length ? selectedElements : elements,
|
||||
appState,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
probablySupportsClipboardWriteText,
|
||||
} from "../clipboard";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
import { exportCanvas } from "../data/index";
|
||||
import { getNonDeletedElements, isTextElement } from "../element";
|
||||
import { t } from "../i18n";
|
||||
@ -15,8 +16,7 @@ export const actionCopy = register({
|
||||
name: "copy",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const elementsToCopy = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
const elementsToCopy = getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
@ -75,11 +75,14 @@ export const actionCopyAsSvg = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard-svg",
|
||||
@ -119,11 +122,14 @@ export const actionCopyAsPng = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await exportCanvas(
|
||||
"clipboard",
|
||||
@ -171,11 +177,14 @@ export const actionCopyAsPng = register({
|
||||
export const copyText = register({
|
||||
name: "copyText",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
});
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
},
|
||||
);
|
||||
|
||||
const text = selectedElements
|
||||
.reduce((acc: string[], element) => {
|
||||
@ -190,15 +199,12 @@ export const copyText = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
predicate: (elements, appState, _, app) => {
|
||||
predicate: (elements, appState) => {
|
||||
return (
|
||||
probablySupportsClipboardWriteText &&
|
||||
app.scene
|
||||
.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
})
|
||||
.some(isTextElement)
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
}).some(isTextElement)
|
||||
);
|
||||
},
|
||||
contextItemLabel: "labels.copyText",
|
||||
|
@ -9,13 +9,19 @@ import { ExcalidrawElement } from "../element/types";
|
||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||
import { t } from "../i18n";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { arrayToMap, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const enableActionGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
return (
|
||||
selectedElements.length > 1 &&
|
||||
// TODO enable distributing frames when implemented properly
|
||||
@ -26,10 +32,12 @@ const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||
const distributeSelectedElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
app: AppClassProperties,
|
||||
distribution: Distribution,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
|
||||
const updatedElements = distributeElements(selectedElements, distribution);
|
||||
|
||||
@ -38,17 +46,16 @@ const distributeSelectedElements = (
|
||||
return updateFrameMembershipOfSelectedElements(
|
||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||
appState,
|
||||
app,
|
||||
);
|
||||
};
|
||||
|
||||
export const distributeHorizontally = register({
|
||||
name: "distributeHorizontally",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: distributeSelectedElements(elements, appState, app, {
|
||||
elements: distributeSelectedElements(elements, appState, {
|
||||
space: "between",
|
||||
axis: "x",
|
||||
}),
|
||||
@ -57,9 +64,9 @@ export const distributeHorizontally = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
hidden={!enableActionGroup(elements, appState)}
|
||||
type="button"
|
||||
icon={DistributeHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
@ -75,10 +82,10 @@ export const distributeHorizontally = register({
|
||||
export const distributeVertically = register({
|
||||
name: "distributeVertically",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
appState,
|
||||
elements: distributeSelectedElements(elements, appState, app, {
|
||||
elements: distributeSelectedElements(elements, appState, {
|
||||
space: "between",
|
||||
axis: "y",
|
||||
}),
|
||||
@ -87,9 +94,9 @@ export const distributeVertically = register({
|
||||
},
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
hidden={!enableActionGroup(elements, appState)}
|
||||
type="button"
|
||||
icon={DistributeVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
|
@ -275,7 +275,6 @@ const duplicateElements = (
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { KEYS } from "../keys";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
@ -10,15 +11,14 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
||||
export const actionToggleElementLock = register({
|
||||
name: "toggleElementLock",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
return !selectedElements.some(
|
||||
(element) => element.locked && element.frameId,
|
||||
);
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
@ -46,9 +46,8 @@ export const actionToggleElementLock = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
contextItemLabel: (elements, appState, app) => {
|
||||
const selected = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
contextItemLabel: (elements, appState) => {
|
||||
const selected = getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: false,
|
||||
});
|
||||
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||
@ -61,13 +60,12 @@ export const actionToggleElementLock = register({
|
||||
? "labels.elementLock.lockAll"
|
||||
: "labels.elementLock.unlockAll";
|
||||
},
|
||||
keyTest: (event, appState, elements, app) => {
|
||||
keyTest: (event, appState, elements) => {
|
||||
return (
|
||||
event.key.toLocaleLowerCase() === KEYS.L &&
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
event.shiftKey &&
|
||||
app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: false,
|
||||
}).length > 0
|
||||
);
|
||||
|
@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
||||
);
|
||||
|
||||
const scaleButtonTitle = `${t(
|
||||
"imageExportDialog.label.scale",
|
||||
"buttons.scale",
|
||||
)} ${s}x (${width}x${height})`;
|
||||
|
||||
return (
|
||||
@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
||||
checked={appState.exportBackground}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("imageExportDialog.label.withBackground")}
|
||||
{t("labels.withBackground")}
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
||||
checked={appState.exportEmbedScene}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("imageExportDialog.label.embedScene")}
|
||||
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||
{t("labels.exportEmbedScene")}
|
||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||
</Tooltip>
|
||||
</CheckboxItem>
|
||||
@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
||||
onChange={(theme: Theme) => {
|
||||
updateData(theme === THEME.DARK);
|
||||
}}
|
||||
title={t("imageExportDialog.label.darkMode")}
|
||||
title={t("labels.toggleExportColorScheme")}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
@ -17,12 +17,11 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||
export const actionFlipHorizontal = register({
|
||||
name: "flipHorizontal",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "horizontal"),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
appState,
|
||||
commitToHistory: true,
|
||||
@ -35,12 +34,11 @@ export const actionFlipHorizontal = register({
|
||||
export const actionFlipVertical = register({
|
||||
name: "flipVertical",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: (elements, appState) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "vertical"),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
appState,
|
||||
commitToHistory: true,
|
||||
|
@ -3,12 +3,19 @@ import { ExcalidrawElement } from "../element/types";
|
||||
import { removeAllElementsFromFrame } from "../frame";
|
||||
import { getFrameElements } from "../frame";
|
||||
import { KEYS } from "../keys";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const isSingleFrameSelected = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
|
||||
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
||||
};
|
||||
@ -16,8 +23,11 @@ const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||
export const actionSelectAllElementsInFrame = register({
|
||||
name: "selectAllElementsInFrame",
|
||||
trackEvent: { category: "canvas" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||
perform: (elements, appState) => {
|
||||
const selectedFrame = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)[0];
|
||||
|
||||
if (selectedFrame && selectedFrame.type === "frame") {
|
||||
const elementsInFrame = getFrameElements(
|
||||
@ -45,15 +55,17 @@ export const actionSelectAllElementsInFrame = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.selectAllElementsInFrame",
|
||||
predicate: (elements, appState, _, app) =>
|
||||
isSingleFrameSelected(appState, app),
|
||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
||||
});
|
||||
|
||||
export const actionRemoveAllElementsFromFrame = register({
|
||||
name: "removeAllElementsFromFrame",
|
||||
trackEvent: { category: "history" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||
perform: (elements, appState) => {
|
||||
const selectedFrame = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)[0];
|
||||
|
||||
if (selectedFrame && selectedFrame.type === "frame") {
|
||||
return {
|
||||
@ -75,8 +87,7 @@ export const actionRemoveAllElementsFromFrame = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.removeAllElementsFromFrame",
|
||||
predicate: (elements, appState, _, app) =>
|
||||
isSingleFrameSelected(appState, app),
|
||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
||||
});
|
||||
|
||||
export const actionupdateFrameRendering = register({
|
||||
|
@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
import { UngroupIcon, GroupIcon } from "../components/icons";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import {
|
||||
getSelectedGroupIds,
|
||||
selectGroup,
|
||||
@ -22,7 +22,7 @@ import {
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import {
|
||||
getElementsInResizingFrame,
|
||||
@ -51,12 +51,14 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||
const enableActionGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
},
|
||||
);
|
||||
return (
|
||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||
);
|
||||
@ -66,10 +68,13 @@ export const actionGroup = register({
|
||||
name: "group",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
},
|
||||
);
|
||||
if (selectedElements.length < 2) {
|
||||
// nothing to group
|
||||
return { appState, elements, commitToHistory: false };
|
||||
@ -159,13 +164,12 @@ export const actionGroup = register({
|
||||
};
|
||||
},
|
||||
contextItemLabel: "labels.group",
|
||||
predicate: (elements, appState, _, app) =>
|
||||
enableActionGroup(elements, appState, app),
|
||||
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
||||
keyTest: (event) =>
|
||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(elements, appState, app)}
|
||||
hidden={!enableActionGroup(elements, appState)}
|
||||
type="button"
|
||||
icon={<GroupIcon theme={appState.theme} />}
|
||||
onClick={() => updateData(null)}
|
||||
@ -187,7 +191,7 @@ export const actionUngroup = register({
|
||||
|
||||
let nextElements = [...elements];
|
||||
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const selectedElements = getSelectedElements(nextElements, appState);
|
||||
const frames = selectedElements
|
||||
.filter((element) => element.frameId)
|
||||
.map((element) =>
|
||||
@ -215,7 +219,6 @@ export const actionUngroup = register({
|
||||
{ ...appState, selectedGroupIds: {} },
|
||||
getNonDeletedElements(nextElements),
|
||||
appState,
|
||||
null,
|
||||
);
|
||||
|
||||
frames.forEach((frame) => {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { ExcalidrawLinearElement } from "../element/types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleLinearEditor = register({
|
||||
@ -8,18 +10,21 @@ export const actionToggleLinearEditor = register({
|
||||
trackEvent: {
|
||||
category: "element",
|
||||
},
|
||||
predicate: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
perform(elements, appState, _, app) {
|
||||
const selectedElement = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
})[0] as ExcalidrawLinearElement;
|
||||
const selectedElement = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
},
|
||||
)[0] as ExcalidrawLinearElement;
|
||||
|
||||
const editingLinearElement =
|
||||
appState.editingLinearElement?.elementId === selectedElement.id
|
||||
@ -33,11 +38,14 @@ export const actionToggleLinearEditor = register({
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
contextItemLabel: (elements, appState, app) => {
|
||||
const selectedElement = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
})[0] as ExcalidrawLinearElement;
|
||||
contextItemLabel: (elements, appState) => {
|
||||
const selectedElement = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
},
|
||||
)[0] as ExcalidrawLinearElement;
|
||||
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||
? "labels.lineEditor.exit"
|
||||
: "labels.lineEditor.edit";
|
||||
|
@ -42,7 +42,6 @@ export const actionSelectAll = register({
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
@ -90,7 +90,6 @@ export class ActionManager {
|
||||
event,
|
||||
this.getAppState(),
|
||||
this.getElementsIncludingDeleted(),
|
||||
this.app,
|
||||
),
|
||||
);
|
||||
|
||||
@ -169,7 +168,6 @@ export class ActionManager {
|
||||
appState={this.getAppState()}
|
||||
updateData={updateData}
|
||||
appProps={this.app.props}
|
||||
app={this.app}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
|
@ -130,7 +130,6 @@ export type PanelComponentProps = {
|
||||
updateData: (formData?: any) => void;
|
||||
appProps: ExcalidrawProps;
|
||||
data?: Record<string, any>;
|
||||
app: AppClassProperties;
|
||||
};
|
||||
|
||||
export interface Action {
|
||||
@ -142,14 +141,12 @@ export interface Action {
|
||||
event: React.KeyboardEvent | KeyboardEvent,
|
||||
appState: AppState,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
app: AppClassProperties,
|
||||
) => boolean;
|
||||
contextItemLabel?:
|
||||
| string
|
||||
| ((
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Readonly<AppState>,
|
||||
app: AppClassProperties,
|
||||
) => string);
|
||||
predicate?: (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
|
@ -330,7 +330,6 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@ -474,6 +473,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
name,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
showHyperlinkPopup: false,
|
||||
defaultSidebarDockedPreference: false,
|
||||
};
|
||||
|
||||
this.id = nanoid();
|
||||
@ -798,7 +799,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
public render() {
|
||||
const selectedElement = this.scene.getSelectedElements(this.state);
|
||||
const selectedElement = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||
|
||||
return (
|
||||
@ -855,7 +859,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!this.state.zenModeEnabled &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
}
|
||||
app={this}
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
@ -961,7 +964,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const shouldUpdateStrokeColor =
|
||||
(type === "background" && event.altKey) ||
|
||||
(type === "stroke" && !event.altKey);
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
if (
|
||||
!selectedElements.length ||
|
||||
this.state.activeTool.type !== "selection"
|
||||
@ -2025,7 +2031,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.canDeviceFitSidebar &&
|
||||
jotaiStore.get(isSidebarDockedAtom)
|
||||
this.state.defaultSidebarDockedPreference
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
@ -2041,7 +2047,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
this,
|
||||
),
|
||||
() => {
|
||||
if (opts.files) {
|
||||
@ -2372,6 +2377,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
toast: {
|
||||
message: string;
|
||||
closable?: boolean;
|
||||
spinner?: boolean;
|
||||
duration?: number;
|
||||
} | null,
|
||||
) => {
|
||||
@ -2606,11 +2612,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
offsetY = step;
|
||||
}
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements({
|
||||
selectedElementIds: this.state.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
{
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
},
|
||||
);
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
mutateElement(element, {
|
||||
@ -2627,7 +2636,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === KEYS.ENTER) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
if (selectedElements.length === 1) {
|
||||
const selectedElement = selectedElements[0];
|
||||
if (event[KEYS.CTRL_OR_CMD]) {
|
||||
@ -2703,7 +2715,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!event.altKey &&
|
||||
!event[KEYS.CTRL_OR_CMD]
|
||||
) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
if (
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!selectedElements.length
|
||||
@ -2775,7 +2790,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ isBindingEnabled: true });
|
||||
}
|
||||
if (isArrowKey(event.key)) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
isBindingEnabled(this.state)
|
||||
? bindOrUnbindSelectedElements(selectedElements)
|
||||
: unbindLinearElements(selectedElements);
|
||||
@ -3125,7 +3143,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
if (isTextElement(selectedElements[0])) {
|
||||
@ -3255,7 +3276,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
if (
|
||||
@ -3306,7 +3330,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@ -3683,7 +3706,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
!isOverScrollBar &&
|
||||
@ -4386,7 +4409,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event: React.PointerEvent<HTMLElement>,
|
||||
): PointerDownState {
|
||||
const origin = viewportCoordsToSceneCoords(event, this.state);
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
||||
|
||||
return {
|
||||
@ -4504,7 +4530,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
): boolean => {
|
||||
if (this.state.activeTool.type === "selection") {
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(elements, this.state);
|
||||
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
||||
const elementWithTransformHandleType =
|
||||
getElementWithTransformHandleType(
|
||||
@ -4747,7 +4773,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
});
|
||||
pointerDownState.hit.wasAddedToSelection = true;
|
||||
@ -5175,7 +5200,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (pointerDownState.drag.offset === null) {
|
||||
pointerDownState.drag.offset = tupleToCoors(
|
||||
getDragOffsetXY(
|
||||
this.scene.getSelectedElements(this.state),
|
||||
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
),
|
||||
@ -5338,7 +5363,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||
!isSelectingPointsInLineEditor
|
||||
) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.every((element) => element.locked)) {
|
||||
return;
|
||||
@ -5409,18 +5437,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const groupIdMap = new Map();
|
||||
const oldIdToDuplicatedId = new Map();
|
||||
const hitElement = pointerDownState.hit.element;
|
||||
const elements = this.scene.getElementsIncludingDeleted();
|
||||
const selectedElementIds = new Set(
|
||||
this.scene
|
||||
.getSelectedElements({
|
||||
selectedElementIds: this.state.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
})
|
||||
.map((element) => element.id),
|
||||
getSelectedElements(elements, this.state, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}).map((element) => element.id),
|
||||
);
|
||||
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
|
||||
for (const element of elements) {
|
||||
if (
|
||||
selectedElementIds.has(element.id) ||
|
||||
@ -5562,7 +5586,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -5620,7 +5643,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -5720,7 +5742,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState.hit?.element?.id !==
|
||||
this.state.selectedLinearElement.elementId
|
||||
) {
|
||||
const selectedELements = this.scene.getSelectedElements(this.state);
|
||||
const selectedELements = getSelectedElements(
|
||||
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
|
||||
if (selectedELements.length > 1) {
|
||||
this.setState({ selectedLinearElement: null });
|
||||
@ -5962,7 +5987,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const topLayerFrame =
|
||||
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
let nextElements = this.scene.getElementsIncludingDeleted();
|
||||
|
||||
const updateGroupIdsAfterEditingGroup = (
|
||||
@ -6041,7 +6069,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
nextElements = updateFrameMembershipOfSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
@ -6086,14 +6113,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let nextElements = updateFrameMembershipOfSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
|
||||
const selectedFrames = this.scene
|
||||
.getSelectedElements(this.state)
|
||||
.filter(
|
||||
(element) => element.type === "frame",
|
||||
) as ExcalidrawFrameElement[];
|
||||
const selectedFrames = getSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
).filter(
|
||||
(element) => element.type === "frame",
|
||||
) as ExcalidrawFrameElement[];
|
||||
|
||||
for (const frame of selectedFrames) {
|
||||
nextElements = replaceAllElementsInFrame(
|
||||
@ -6118,7 +6145,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
||||
isLinearElement(hitElement)
|
||||
) {
|
||||
const selectedELements = this.scene.getSelectedElements(this.state);
|
||||
const selectedELements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
// set selectedLinearElement when no other element selected except
|
||||
// the one we've hit
|
||||
if (selectedELements.length === 1) {
|
||||
@ -6220,7 +6250,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
delete newSelectedElementIds[hitElement!.id];
|
||||
const newSelectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
{ selectedElementIds: newSelectedElementIds },
|
||||
{ ...prevState, selectedElementIds: newSelectedElementIds },
|
||||
);
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
@ -6239,7 +6269,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -6276,7 +6305,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@ -6307,7 +6335,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
}));
|
||||
}
|
||||
@ -6367,7 +6394,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
|
||||
(isBindingEnabled(this.state)
|
||||
? bindOrUnbindSelectedElements
|
||||
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
|
||||
: unbindLinearElements)(
|
||||
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||
@ -7074,7 +7103,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
includeLockedElements: true,
|
||||
});
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const isHittignCommonBoundBox =
|
||||
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||
{ x, y },
|
||||
@ -7104,7 +7136,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
this,
|
||||
)
|
||||
: this.state),
|
||||
showHyperlinkPopup: false,
|
||||
@ -7192,7 +7223,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState: PointerDownState,
|
||||
event: MouseEvent | KeyboardEvent,
|
||||
): boolean => {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
const selectedFrames = selectedElements.filter(
|
||||
(element) => element.type === "frame",
|
||||
) as ExcalidrawFrameElement[];
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from "./colorPickerUtils";
|
||||
import HotkeyLabel from "./HotkeyLabel";
|
||||
import { ColorPaletteCustom } from "../../colors";
|
||||
import { TranslationKeys, t } from "../../i18n";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
interface PickerColorListProps {
|
||||
palette: ColorPaletteCustom;
|
||||
@ -48,11 +48,7 @@ const PickerColorList = ({
|
||||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||
|
||||
const keybinding = colorPickerHotkeyBindings[index];
|
||||
const label = t(
|
||||
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
|
||||
null,
|
||||
"",
|
||||
);
|
||||
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t, TranslationKeys } from "../i18n";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
@ -82,15 +82,9 @@ export const ContextMenu = React.memo(
|
||||
let label = "";
|
||||
if (item.contextItemLabel) {
|
||||
if (typeof item.contextItemLabel === "function") {
|
||||
label = t(
|
||||
item.contextItemLabel(
|
||||
elements,
|
||||
appState,
|
||||
actionManager.app,
|
||||
) as unknown as TranslationKeys,
|
||||
);
|
||||
label = t(item.contextItemLabel(elements, appState));
|
||||
} else {
|
||||
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||
label = t(item.contextItemLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { t } from "../i18n";
|
||||
import { AppClassProperties, Device, UIAppState } from "../types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { Device, UIAppState } from "../types";
|
||||
import {
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
@ -13,12 +15,17 @@ import "./HintViewer.scss";
|
||||
|
||||
interface HintViewerProps {
|
||||
appState: UIAppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
isMobile: boolean;
|
||||
device: Device;
|
||||
app: AppClassProperties;
|
||||
}
|
||||
|
||||
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||
const getHints = ({
|
||||
appState,
|
||||
elements,
|
||||
isMobile,
|
||||
device,
|
||||
}: HintViewerProps) => {
|
||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
const multiMode = appState.multiElement !== null;
|
||||
|
||||
@ -48,7 +55,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||
return t("hints.placeImage");
|
||||
}
|
||||
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
if (
|
||||
isResizing &&
|
||||
@ -108,15 +115,15 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||
|
||||
export const HintViewer = ({
|
||||
appState,
|
||||
elements,
|
||||
isMobile,
|
||||
device,
|
||||
app,
|
||||
}: HintViewerProps) => {
|
||||
let hint = getHints({
|
||||
appState,
|
||||
elements,
|
||||
isMobile,
|
||||
device,
|
||||
app,
|
||||
});
|
||||
if (!hint) {
|
||||
return null;
|
||||
|
@ -72,7 +72,6 @@ interface LayerUIProps {
|
||||
onExportImage: AppClassProperties["onExportImage"];
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
app: AppClassProperties;
|
||||
}
|
||||
|
||||
const DefaultMainMenu: React.FC<{
|
||||
@ -128,7 +127,6 @@ const LayerUI = ({
|
||||
onExportImage,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
app,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
@ -242,9 +240,9 @@ const LayerUI = ({
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
isMobile={device.isMobile}
|
||||
device={device}
|
||||
app={app}
|
||||
/>
|
||||
{heading}
|
||||
<Stack.Row gap={1}>
|
||||
@ -403,7 +401,6 @@ const LayerUI = ({
|
||||
)}
|
||||
{device.isMobile && (
|
||||
<MobileMenu
|
||||
app={app}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
|
@ -1,11 +1,5 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
UIAppState,
|
||||
} from "../types";
|
||||
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
@ -47,7 +41,6 @@ type MobileMenuProps = {
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen: boolean;
|
||||
app: AppClassProperties;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@ -65,7 +58,6 @@ export const MobileMenu = ({
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
app,
|
||||
}: MobileMenuProps) => {
|
||||
const {
|
||||
WelcomeScreenCenterTunnel,
|
||||
@ -127,9 +119,9 @@ export const MobileMenu = ({
|
||||
</Section>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
isMobile={true}
|
||||
device={device}
|
||||
app={app}
|
||||
/>
|
||||
</FixedSideContainer>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { t } from "../i18n";
|
||||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
export const Section: React.FC<{
|
||||
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||
heading: string;
|
||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||
className?: string;
|
||||
}> = ({ heading, children, ...props }) => {
|
||||
|
@ -25,6 +25,17 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.Toast__message--spinner {
|
||||
padding: 0 3rem;
|
||||
}
|
||||
|
||||
.Toast__spinner {
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { CloseIcon } from "./icons";
|
||||
import Spinner from "./Spinner";
|
||||
import "./Toast.scss";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
@ -9,12 +11,14 @@ export const Toast = ({
|
||||
message,
|
||||
onClose,
|
||||
closable = false,
|
||||
spinner = true,
|
||||
// To prevent autoclose, pass duration as Infinity
|
||||
duration = DEFAULT_TOAST_TIMEOUT,
|
||||
}: {
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
closable?: boolean;
|
||||
spinner?: boolean;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const timerRef = useRef<number>(0);
|
||||
@ -44,7 +48,18 @@ export const Toast = ({
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<p className="Toast__message">{message}</p>
|
||||
{spinner && (
|
||||
<div className="Toast__spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className={clsx("Toast__message", {
|
||||
"Toast__message--spinner": spinner,
|
||||
})}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
{closable && (
|
||||
<ToolButton
|
||||
icon={CloseIcon}
|
||||
|
@ -3,7 +3,6 @@ import { render } from "@testing-library/react";
|
||||
import fallbackLangData from "../locales/en.json";
|
||||
|
||||
import Trans from "./Trans";
|
||||
import { TranslationKeys } from "../i18n";
|
||||
|
||||
describe("Test <Trans/>", () => {
|
||||
it("should translate the the strings correctly", () => {
|
||||
@ -19,27 +18,24 @@ describe("Test <Trans/>", () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<div data-testid="test1">
|
||||
<Trans
|
||||
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||
audience="world"
|
||||
/>
|
||||
<Trans i18nKey="transTest.key1" audience="world" />
|
||||
</div>
|
||||
<div data-testid="test2">
|
||||
<Trans
|
||||
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||
i18nKey="transTest.key2"
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test3">
|
||||
<Trans
|
||||
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||
i18nKey="transTest.key3"
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test4">
|
||||
<Trans
|
||||
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||
i18nKey="transTest.key4"
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
bold={(el) => <strong>{el}</strong>}
|
||||
@ -47,7 +43,7 @@ describe("Test <Trans/>", () => {
|
||||
</div>
|
||||
<div data-testid="test5">
|
||||
<Trans
|
||||
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||
i18nKey="transTest.key5"
|
||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { TranslationKeys, useI18n } from "../i18n";
|
||||
import { useI18n } from "../i18n";
|
||||
|
||||
// Used for splitting i18nKey into tokens in Trans component
|
||||
// Example:
|
||||
@ -153,7 +153,7 @@ const Trans = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
i18nKey: TranslationKeys;
|
||||
i18nKey: string;
|
||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
@ -7,6 +7,8 @@ export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
|
||||
export const SYNC_BROWSER_TABS_TIMEOUT = 50;
|
||||
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
|
||||
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
|
||||
export const PAUSE_COLLABORATION_TIMEOUT = 30000;
|
||||
export const RESUME_FALLBACK_TIMEOUT = 5000;
|
||||
|
||||
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
|
||||
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
import { ExcalidrawImperativeAPI } from "../../types";
|
||||
import { ExcalidrawImperativeAPI, PauseCollaborationState } from "../../types";
|
||||
import { ErrorDialog } from "../../components/ErrorDialog";
|
||||
import { APP_NAME, ENV, EVENT } from "../../constants";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
@ -16,6 +16,7 @@ import { Collaborator, Gesture } from "../../types";
|
||||
import {
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
upsertMap,
|
||||
withBatchedUpdates,
|
||||
} from "../../utils";
|
||||
import {
|
||||
@ -24,12 +25,15 @@ import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||
LOAD_IMAGES_TIMEOUT,
|
||||
PAUSE_COLLABORATION_TIMEOUT,
|
||||
WS_SCENE_EVENT_TYPES,
|
||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||
RESUME_FALLBACK_TIMEOUT,
|
||||
} from "../app_constants";
|
||||
import {
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getCollaborationLinkData,
|
||||
getCollabServer,
|
||||
getSyncableElements,
|
||||
SocketUpdateDataSource,
|
||||
@ -43,8 +47,8 @@ import {
|
||||
saveToFirebase,
|
||||
} from "../data/firebase";
|
||||
import {
|
||||
importUsernameFromLocalStorage,
|
||||
saveUsernameToLocalStorage,
|
||||
importUsernameAndIdFromLocalStorage,
|
||||
saveUsernameAndIdToLocalStorage,
|
||||
} from "../data/localStorage";
|
||||
import Portal from "./Portal";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
@ -71,16 +75,19 @@ import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { appJotaiStore } from "../app-jotai";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const collabDialogShownAtom = atom(false);
|
||||
export const isCollaboratingAtom = atom(false);
|
||||
export const isOfflineAtom = atom(false);
|
||||
export const isCollaborationPausedAtom = atom(false);
|
||||
|
||||
interface CollabState {
|
||||
errorMessage: string;
|
||||
username: string;
|
||||
activeRoomLink: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
type CollabInstance = InstanceType<typeof Collab>;
|
||||
@ -94,6 +101,7 @@ export interface CollabAPI {
|
||||
syncElements: CollabInstance["syncElements"];
|
||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||
setUsername: (username: string) => void;
|
||||
isPaused: () => boolean;
|
||||
}
|
||||
|
||||
interface PublicProps {
|
||||
@ -108,6 +116,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
excalidrawAPI: Props["excalidrawAPI"];
|
||||
activeIntervalId: number | null;
|
||||
idleTimeoutId: number | null;
|
||||
pauseTimeoutId: number | null;
|
||||
|
||||
private socketInitializationTimer?: number;
|
||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||
@ -115,9 +124,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { username, userId } = importUsernameAndIdFromLocalStorage() || {};
|
||||
|
||||
this.state = {
|
||||
errorMessage: "",
|
||||
username: importUsernameFromLocalStorage() || "",
|
||||
username: username || "",
|
||||
userId: userId || "",
|
||||
activeRoomLink: "",
|
||||
};
|
||||
this.portal = new Portal(this);
|
||||
@ -149,6 +162,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
this.excalidrawAPI = props.excalidrawAPI;
|
||||
this.activeIntervalId = null;
|
||||
this.idleTimeoutId = null;
|
||||
this.pauseTimeoutId = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -167,6 +181,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
||||
stopCollaboration: this.stopCollaboration,
|
||||
setUsername: this.setUsername,
|
||||
isPaused: this.isPaused,
|
||||
};
|
||||
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
@ -207,6 +222,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
window.clearTimeout(this.idleTimeoutId);
|
||||
this.idleTimeoutId = null;
|
||||
}
|
||||
if (this.pauseTimeoutId) {
|
||||
window.clearTimeout(this.pauseTimeoutId);
|
||||
this.pauseTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||
@ -310,6 +329,126 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
fallbackResumeTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
/**
|
||||
* Handles the pause and resume states of a collaboration session.
|
||||
* This function gets triggered when a change in the collaboration pause state is detected.
|
||||
* Based on the state, the function carries out the following actions:
|
||||
* 1. `PAUSED`: Saves the current scene to Firebase, disconnects the socket, and updates the scene to view mode.
|
||||
* 2. `RESUMED`: Connects the socket, shows a toast message, sets a fallback to fetch data from Firebase, and resets the pause timeout if any.
|
||||
* 3. `SYNCED`: Clears the fallback timeout if any, updates the collaboration pause state, and updates the scene to editing mode.
|
||||
*
|
||||
* @param state - The new state of the collaboration session. It is one of the values of `PauseCollaborationState` enum, which includes `PAUSED`, `RESUMED`, and `SYNCED`.
|
||||
*/
|
||||
onPauseCollaborationChange = (state: PauseCollaborationState) => {
|
||||
switch (state) {
|
||||
case PauseCollaborationState.PAUSED: {
|
||||
if (this.portal.socket) {
|
||||
// Save current scene to firebase
|
||||
this.saveCollabRoomToFirebase(
|
||||
getSyncableElements(
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
|
||||
this.portal.socket.disconnect();
|
||||
this.portal.socketInitialized = false;
|
||||
this.setIsCollaborationPaused(true);
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
appState: { viewModeEnabled: true },
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PauseCollaborationState.RESUMED: {
|
||||
if (this.portal.socket && this.isPaused()) {
|
||||
this.portal.socket.connect();
|
||||
this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT);
|
||||
|
||||
this.excalidrawAPI.setToast({
|
||||
message: t("toast.reconnectRoomServer"),
|
||||
duration: Infinity,
|
||||
spinner: true,
|
||||
closable: false,
|
||||
});
|
||||
|
||||
// Fallback to fetch data from firebase when reconnecting to scene without collaborators
|
||||
const fallbackResumeHandler = async () => {
|
||||
const roomLinkData = getCollaborationLinkData(
|
||||
this.state.activeRoomLink,
|
||||
);
|
||||
if (!roomLinkData) {
|
||||
return;
|
||||
}
|
||||
const elements = await loadFromFirebase(
|
||||
roomLinkData.roomId,
|
||||
roomLinkData.roomKey,
|
||||
this.portal.socket,
|
||||
);
|
||||
if (elements) {
|
||||
this.setLastBroadcastedOrReceivedSceneVersion(
|
||||
getSceneVersion(elements),
|
||||
);
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
});
|
||||
}
|
||||
this.onPauseCollaborationChange(PauseCollaborationState.SYNCED);
|
||||
};
|
||||
|
||||
// Set timeout to fallback to fetch data from firebase
|
||||
this.fallbackResumeTimeout = setTimeout(
|
||||
fallbackResumeHandler,
|
||||
RESUME_FALLBACK_TIMEOUT,
|
||||
);
|
||||
|
||||
// When no users are in the room, we fallback to fetch data from firebase immediately and clear fallback timeout
|
||||
this.portal.socket.on("first-in-room", () => {
|
||||
if (this.portal.socket) {
|
||||
this.portal.socket.off("first-in-room");
|
||||
// Recall init event to initialize collab with other users (fixes https://github.com/excalidraw/excalidraw/pull/6638#issuecomment-1600799080)
|
||||
this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT);
|
||||
}
|
||||
|
||||
fallbackResumeHandler();
|
||||
});
|
||||
}
|
||||
|
||||
// Clear pause timeout if exists
|
||||
if (this.pauseTimeoutId) {
|
||||
clearTimeout(this.pauseTimeoutId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case PauseCollaborationState.SYNCED: {
|
||||
if (this.fallbackResumeTimeout) {
|
||||
clearTimeout(this.fallbackResumeTimeout);
|
||||
this.fallbackResumeTimeout = null;
|
||||
}
|
||||
|
||||
if (this.isPaused()) {
|
||||
this.setIsCollaborationPaused(false);
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
appState: { viewModeEnabled: false },
|
||||
});
|
||||
this.excalidrawAPI.setToast(null);
|
||||
this.excalidrawAPI.scrollToContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
isPaused = () => appJotaiStore.get(isCollaborationPausedAtom)!;
|
||||
|
||||
setIsCollaborationPaused = (isPaused: boolean) => {
|
||||
appJotaiStore.set(isCollaborationPausedAtom, isPaused);
|
||||
};
|
||||
|
||||
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||
this.portal.close();
|
||||
@ -388,6 +527,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.state.userId) {
|
||||
const userId = nanoid();
|
||||
this.onUserIdChange(userId);
|
||||
}
|
||||
|
||||
if (this.portal.socket) {
|
||||
return null;
|
||||
}
|
||||
@ -502,6 +646,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
elements: reconciledElements,
|
||||
scrollToContent: true,
|
||||
});
|
||||
this.onPauseCollaborationChange(PauseCollaborationState.SYNCED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -511,36 +656,63 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
break;
|
||||
case "MOUSE_LOCATION": {
|
||||
const { pointer, button, username, selectedElementIds } =
|
||||
decryptedData.payload;
|
||||
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
||||
decryptedData.payload.socketId ||
|
||||
// @ts-ignore legacy, see #2094 (#2097)
|
||||
decryptedData.payload.socketID;
|
||||
|
||||
const collaborators = new Map(this.collaborators);
|
||||
const user = collaborators.get(socketId) || {}!;
|
||||
user.pointer = pointer;
|
||||
user.button = button;
|
||||
user.selectedElementIds = selectedElementIds;
|
||||
user.username = username;
|
||||
collaborators.set(socketId, user);
|
||||
const {
|
||||
pointer,
|
||||
button,
|
||||
username,
|
||||
selectedElementIds,
|
||||
userId,
|
||||
socketId,
|
||||
} = decryptedData.payload;
|
||||
const collaborators = upsertMap(
|
||||
userId,
|
||||
{
|
||||
username,
|
||||
pointer,
|
||||
button,
|
||||
selectedElementIds,
|
||||
socketId,
|
||||
},
|
||||
this.collaborators,
|
||||
);
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
collaborators: new Map(collaborators),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "IDLE_STATUS": {
|
||||
const { userState, socketId, username } = decryptedData.payload;
|
||||
const collaborators = new Map(this.collaborators);
|
||||
const user = collaborators.get(socketId) || {}!;
|
||||
user.userState = userState;
|
||||
user.username = username;
|
||||
const { userState, username, userId, socketId } =
|
||||
decryptedData.payload;
|
||||
const collaborators = upsertMap(
|
||||
userId,
|
||||
{
|
||||
username,
|
||||
userState,
|
||||
userId,
|
||||
socketId,
|
||||
},
|
||||
this.collaborators,
|
||||
);
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators,
|
||||
collaborators: new Map(collaborators),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "USER_JOINED": {
|
||||
const { username, userId, socketId } = decryptedData.payload;
|
||||
const collaborators = upsertMap(
|
||||
userId,
|
||||
{
|
||||
username,
|
||||
userId,
|
||||
socketId,
|
||||
},
|
||||
this.collaborators,
|
||||
);
|
||||
this.excalidrawAPI.updateScene({
|
||||
collaborators: new Map(collaborators),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -609,6 +781,15 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
} else {
|
||||
this.portal.socketInitialized = true;
|
||||
}
|
||||
|
||||
if (this.portal.socket) {
|
||||
this.portal.brodcastUserJoinedRoom({
|
||||
username: this.state.username,
|
||||
userId: this.state.userId,
|
||||
socketId: this.portal.socket.id,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -696,6 +877,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
window.clearInterval(this.activeIntervalId);
|
||||
this.activeIntervalId = null;
|
||||
}
|
||||
this.pauseTimeoutId = window.setTimeout(
|
||||
() => this.onPauseCollaborationChange(PauseCollaborationState.PAUSED),
|
||||
PAUSE_COLLABORATION_TIMEOUT,
|
||||
);
|
||||
this.onIdleStateChange(UserIdleState.AWAY);
|
||||
} else {
|
||||
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
||||
@ -704,6 +889,11 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
ACTIVE_THRESHOLD,
|
||||
);
|
||||
this.onIdleStateChange(UserIdleState.ACTIVE);
|
||||
if (this.pauseTimeoutId) {
|
||||
window.clearTimeout(this.pauseTimeoutId);
|
||||
this.onPauseCollaborationChange(PauseCollaborationState.RESUMED);
|
||||
this.pauseTimeoutId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -725,17 +915,12 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
};
|
||||
|
||||
setCollaborators(sockets: string[]) {
|
||||
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
||||
new Map();
|
||||
for (const socketId of sockets) {
|
||||
if (this.collaborators.has(socketId)) {
|
||||
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
||||
} else {
|
||||
collaborators.set(socketId, {});
|
||||
this.collaborators.forEach((value, key) => {
|
||||
if (value.socketId && !sockets.includes(value.socketId)) {
|
||||
this.collaborators.delete(key);
|
||||
}
|
||||
}
|
||||
this.collaborators = collaborators;
|
||||
this.excalidrawAPI.updateScene({ collaborators });
|
||||
});
|
||||
this.excalidrawAPI.updateScene({ collaborators: this.collaborators });
|
||||
}
|
||||
|
||||
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
||||
@ -821,7 +1006,12 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
onUsernameChange = (username: string) => {
|
||||
this.setUsername(username);
|
||||
saveUsernameToLocalStorage(username);
|
||||
saveUsernameAndIdToLocalStorage(username, this.state.userId);
|
||||
};
|
||||
|
||||
onUserIdChange = (userId: string) => {
|
||||
this.setState({ userId });
|
||||
saveUsernameAndIdToLocalStorage(this.state.username, userId);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -37,6 +37,29 @@ class Portal {
|
||||
this.roomId = id;
|
||||
this.roomKey = key;
|
||||
|
||||
this.initializeSocketListeners();
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
this.queueFileUpload.flush();
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.roomId = null;
|
||||
this.roomKey = null;
|
||||
this.socketInitialized = false;
|
||||
this.broadcastedElementVersions = new Map();
|
||||
}
|
||||
|
||||
initializeSocketListeners() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize socket listeners
|
||||
this.socket.on("init-room", () => {
|
||||
if (this.socket) {
|
||||
@ -54,21 +77,6 @@ class Portal {
|
||||
this.socket.on("room-user-change", (clients: string[]) => {
|
||||
this.collab.setCollaborators(clients);
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
this.queueFileUpload.flush();
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.roomId = null;
|
||||
this.roomKey = null;
|
||||
this.socketInitialized = false;
|
||||
this.broadcastedElementVersions = new Map();
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
@ -181,13 +189,14 @@ class Portal {
|
||||
};
|
||||
|
||||
broadcastIdleChange = (userState: UserIdleState) => {
|
||||
if (this.socket?.id) {
|
||||
if (this.socket) {
|
||||
const data: SocketUpdateDataSource["IDLE_STATUS"] = {
|
||||
type: "IDLE_STATUS",
|
||||
payload: {
|
||||
socketId: this.socket.id,
|
||||
userState,
|
||||
username: this.collab.state.username,
|
||||
userId: this.collab.state.userId,
|
||||
socketId: this.socket.id,
|
||||
},
|
||||
};
|
||||
return this._broadcastSocketData(
|
||||
@ -201,16 +210,17 @@ class Portal {
|
||||
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
||||
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
||||
}) => {
|
||||
if (this.socket?.id) {
|
||||
if (this.socket) {
|
||||
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
|
||||
type: "MOUSE_LOCATION",
|
||||
payload: {
|
||||
socketId: this.socket.id,
|
||||
pointer: payload.pointer,
|
||||
button: payload.button || "up",
|
||||
selectedElementIds:
|
||||
this.collab.excalidrawAPI.getAppState().selectedElementIds,
|
||||
username: this.collab.state.username,
|
||||
userId: this.collab.state.userId,
|
||||
socketId: this.socket.id,
|
||||
},
|
||||
};
|
||||
return this._broadcastSocketData(
|
||||
@ -219,6 +229,23 @@ class Portal {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
brodcastUserJoinedRoom = (payload: {
|
||||
username: string;
|
||||
userId: string;
|
||||
socketId: string;
|
||||
}) => {
|
||||
if (this.socket) {
|
||||
const data: SocketUpdateDataSource["USER_JOINED"] = {
|
||||
type: "USER_JOINED",
|
||||
payload,
|
||||
};
|
||||
return this._broadcastSocketData(
|
||||
data as SocketUpdateData,
|
||||
false, // volatile
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Portal;
|
||||
|
@ -106,19 +106,29 @@ export type SocketUpdateDataSource = {
|
||||
MOUSE_LOCATION: {
|
||||
type: "MOUSE_LOCATION";
|
||||
payload: {
|
||||
socketId: string;
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
username: string;
|
||||
userId: string;
|
||||
socketId: string;
|
||||
};
|
||||
};
|
||||
IDLE_STATUS: {
|
||||
type: "IDLE_STATUS";
|
||||
payload: {
|
||||
socketId: string;
|
||||
userState: UserIdleState;
|
||||
username: string;
|
||||
userId: string;
|
||||
socketId: string;
|
||||
};
|
||||
};
|
||||
USER_JOINED: {
|
||||
type: "USER_JOINED";
|
||||
payload: {
|
||||
username: string;
|
||||
userId: string;
|
||||
socketId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -8,11 +8,14 @@ import { clearElementsForLocalStorage } from "../../element";
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
|
||||
export const saveUsernameToLocalStorage = (username: string) => {
|
||||
export const saveUsernameAndIdToLocalStorage = (
|
||||
username: string,
|
||||
userId: string,
|
||||
) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_COLLAB,
|
||||
JSON.stringify({ username }),
|
||||
JSON.stringify({ username, userId }),
|
||||
);
|
||||
} catch (error: any) {
|
||||
// Unable to access window.localStorage
|
||||
@ -20,11 +23,14 @@ export const saveUsernameToLocalStorage = (username: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const importUsernameFromLocalStorage = (): string | null => {
|
||||
export const importUsernameAndIdFromLocalStorage = (): {
|
||||
username: string;
|
||||
userId: string;
|
||||
} | null => {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||
if (data) {
|
||||
return JSON.parse(data).username;
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Unable to access localStorage
|
||||
|
@ -65,7 +65,7 @@ import {
|
||||
import {
|
||||
getLibraryItemsFromStorage,
|
||||
importFromLocalStorage,
|
||||
importUsernameFromLocalStorage,
|
||||
importUsernameAndIdFromLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||
@ -411,7 +411,8 @@ const ExcalidrawWrapper = () => {
|
||||
// don't sync if local state is newer or identical to browser state
|
||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||
const localDataState = importFromLocalStorage();
|
||||
const username = importUsernameFromLocalStorage();
|
||||
const username =
|
||||
importUsernameAndIdFromLocalStorage()?.username ?? "";
|
||||
let langCode = languageDetector.detect() || defaultLang.code;
|
||||
if (Array.isArray(langCode)) {
|
||||
langCode = langCode[0];
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from "./element/textElement";
|
||||
import { arrayToMap, findIndex } from "./utils";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import { AppClassProperties, AppState } from "./types";
|
||||
import { AppState } from "./types";
|
||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
import { isFrameElement } from "./element";
|
||||
import { moveOneRight } from "./zindex";
|
||||
@ -571,13 +571,8 @@ export const replaceAllElementsInFrame = (
|
||||
export const updateFrameMembershipOfSelectedElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
// supplying elements explicitly in case we're passed non-state elements
|
||||
elements: allElements,
|
||||
});
|
||||
const selectedElements = getSelectedElements(allElements, appState);
|
||||
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
|
||||
|
||||
if (appState.editingGroupId) {
|
||||
|
@ -1,10 +1,5 @@
|
||||
import {
|
||||
GroupId,
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { AppClassProperties, AppState } from "./types";
|
||||
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||
import { AppState } from "./types";
|
||||
import { getSelectedElements } from "./scene";
|
||||
import { getBoundTextElement } from "./element/textElement";
|
||||
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||
@ -72,23 +67,12 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
||||
*/
|
||||
export const selectGroupsForSelectedElements = (
|
||||
appState: AppState,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||
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 => {
|
||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
||||
|
||||
const selectedElements = app
|
||||
? app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
// supplying elements explicitly in case we're passed non-state elements
|
||||
elements,
|
||||
})
|
||||
: getSelectedElements(elements, appState);
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
if (!selectedElements.length) {
|
||||
return {
|
||||
|
@ -3,7 +3,6 @@ import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
import { jotaiScope, jotaiStore } from "./jotai";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { NestedKeyOf } from "./utility-types";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
@ -13,8 +12,6 @@ export interface Language {
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||
|
||||
export const defaultLang = { code: "en", label: "English" };
|
||||
|
||||
export const languages: Language[] = [
|
||||
@ -126,7 +123,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||
};
|
||||
|
||||
export const t = (
|
||||
path: NestedKeyOf<typeof fallbackLangData>,
|
||||
path: string,
|
||||
replacement?: { [key: string]: string | number } | null,
|
||||
fallback?: string,
|
||||
) => {
|
||||
|
@ -411,7 +411,8 @@
|
||||
"fileSavedToFilename": "Saved to {filename}",
|
||||
"canvas": "canvas",
|
||||
"selection": "selection",
|
||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor"
|
||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||
"reconnectRoomServer": "Reconnecting to server"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
|
@ -1,16 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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,7 +52,6 @@
|
||||
"@babel/preset-env": "7.18.6",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@size-limit/preset-big-lib": "8.2.6",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
@ -62,8 +61,6 @@
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"size-limit": "8.2.4",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.3",
|
||||
"ts-loader": "9.3.1",
|
||||
"typescript": "4.9.4",
|
||||
@ -82,7 +79,6 @@
|
||||
"pack": "yarn build:umd && yarn pack",
|
||||
"start": "webpack serve --config webpack.dev-server.config.js",
|
||||
"install:deps": "yarn install --frozen-lockfile && yarn --cwd ../../../",
|
||||
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types",
|
||||
"size": "yarn build:umd && size-limit"
|
||||
"build:example": "EXAMPLE=true webpack --config webpack.dev-server.config.js && yarn gen:types"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -619,8 +619,8 @@ export const _renderScene = ({
|
||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||
selectionColors.push(
|
||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||
(socketId) => {
|
||||
const background = getClientColor(socketId);
|
||||
(userId) => {
|
||||
const background = getClientColor(userId);
|
||||
return background;
|
||||
},
|
||||
),
|
||||
|
@ -11,9 +11,6 @@ import {
|
||||
} from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
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 ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
@ -21,31 +18,6 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
type SceneStateCallback = () => 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
|
||||
// in our codebase
|
||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||
@ -96,15 +68,6 @@ class Scene {
|
||||
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
|
||||
private frames: readonly ExcalidrawFrameElement[] = [];
|
||||
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() {
|
||||
return this.elements;
|
||||
@ -118,52 +81,6 @@ class Scene {
|
||||
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>[] {
|
||||
return this.nonDeletedFrames;
|
||||
}
|
||||
@ -251,21 +168,11 @@ class Scene {
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (scene === this) {
|
||||
Scene.sceneMapById.delete(elementKey);
|
||||
}
|
||||
});
|
||||
|
||||
// done not for memory leaks, but to guard against possible late fires
|
||||
// (I guess?)
|
||||
this.callbacks.clear();
|
||||
|
@ -1527,14 +1527,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -1586,14 +1586,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4271,14 +4271,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 238820263,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4303,14 +4303,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 238820263,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@ -4362,14 +4362,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4405,14 +4405,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4434,14 +4434,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 401146281,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@ -4482,14 +4482,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4513,14 +4513,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@ -4557,14 +4557,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 238820263,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -4586,14 +4586,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 238820263,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@ -5585,14 +5585,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -5619,14 +5619,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 238820263,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
@ -5678,14 +5678,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -5721,14 +5721,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -5750,14 +5750,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 401146281,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
@ -5798,14 +5798,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@ -5829,14 +5829,14 @@ Object {
|
||||
"roundness": Object {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 401146281,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 238820263,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -90,7 +90,6 @@ const populateElements = (
|
||||
{ ...h.state, ...appState, selectedElementIds },
|
||||
h.elements,
|
||||
h.state,
|
||||
null,
|
||||
),
|
||||
...appState,
|
||||
selectedElementIds,
|
||||
|
@ -55,6 +55,7 @@ export type Collaborator = {
|
||||
avatarUrl?: string;
|
||||
// user id. If supplied, we'll filter out duplicates when rendering user avatars.
|
||||
id?: string;
|
||||
socketId?: string;
|
||||
};
|
||||
|
||||
export type DataURL = string & { _brand: "DataURL" };
|
||||
@ -376,6 +377,12 @@ export enum UserIdleState {
|
||||
IDLE = "idle",
|
||||
}
|
||||
|
||||
export enum PauseCollaborationState {
|
||||
PAUSED = "paused",
|
||||
RESUMED = "resumed",
|
||||
SYNCED = "synced",
|
||||
}
|
||||
|
||||
export type ExportOpts = {
|
||||
saveFileToDisk?: boolean;
|
||||
onExportToBackend?: (
|
||||
|
@ -47,10 +47,3 @@ export type ForwardRef<T, P = any> = Parameters<
|
||||
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
||||
? U
|
||||
: 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;
|
||||
|
11
src/utils.ts
11
src/utils.ts
@ -907,3 +907,14 @@ export const isOnlyExportingSingleFrame = (
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const upsertMap = <T>(key: T, value: object, map: Map<T, object>) => {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, value);
|
||||
} else {
|
||||
const old = map.get(key);
|
||||
map.set(key, { ...old, ...value });
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
205
yarn.lock
205
yarn.lock
@ -2329,6 +2329,11 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
"@socket.io/component-emitter@~3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
||||
|
||||
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
||||
@ -3168,11 +3173,6 @@ adjust-sourcemap-loader@^4.0.0:
|
||||
loader-utils "^2.0.0"
|
||||
regex-parser "^2.2.11"
|
||||
|
||||
after@0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
|
||||
integrity sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
@ -3398,11 +3398,6 @@ array.prototype.tosorted@^1.1.1:
|
||||
es-shim-unscopables "^1.0.0"
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
arraybuffer.slice@~0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
|
||||
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
|
||||
|
||||
asap@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
@ -3423,11 +3418,6 @@ astral-regex@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async-limiter@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||
|
||||
async@^2.6.4:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
@ -3620,11 +3610,6 @@ babel-preset-react-app@^10.0.1:
|
||||
babel-plugin-macros "^3.1.0"
|
||||
babel-plugin-transform-react-remove-prop-types "^0.4.24"
|
||||
|
||||
backo2@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
|
||||
integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
@ -3635,11 +3620,6 @@ base64-arraybuffer-es6@^0.7.0:
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86"
|
||||
integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==
|
||||
|
||||
base64-arraybuffer@0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
|
||||
integrity sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==
|
||||
|
||||
basic-auth@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
|
||||
@ -3672,11 +3652,6 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
blob@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
||||
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
|
||||
|
||||
bluebird@^3.5.5:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
@ -4077,21 +4052,6 @@ commondir@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
|
||||
|
||||
component-bind@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
|
||||
integrity sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==
|
||||
|
||||
component-emitter@~1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||
|
||||
component-inherit@0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
|
||||
integrity sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==
|
||||
|
||||
compressible@~2.0.16:
|
||||
version "2.0.18"
|
||||
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
|
||||
@ -4464,7 +4424,7 @@ debug@2.6.9, debug@^2.6.0:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
@ -4478,13 +4438,6 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@~3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
decimal.js@^10.2.1:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
|
||||
@ -4818,33 +4771,21 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
engine.io-client@~3.4.0:
|
||||
version "3.4.4"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967"
|
||||
integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==
|
||||
engine.io-client@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91"
|
||||
integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==
|
||||
dependencies:
|
||||
component-emitter "~1.3.0"
|
||||
component-inherit "0.0.3"
|
||||
debug "~3.1.0"
|
||||
engine.io-parser "~2.2.0"
|
||||
has-cors "1.1.0"
|
||||
indexof "0.0.1"
|
||||
parseqs "0.0.6"
|
||||
parseuri "0.0.6"
|
||||
ws "~6.1.0"
|
||||
xmlhttprequest-ssl "~1.5.4"
|
||||
yeast "0.1.2"
|
||||
"@socket.io/component-emitter" "~3.1.0"
|
||||
debug "~4.3.1"
|
||||
engine.io-parser "~5.0.3"
|
||||
ws "~8.11.0"
|
||||
xmlhttprequest-ssl "~2.0.0"
|
||||
|
||||
engine.io-parser@~2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
|
||||
integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
|
||||
dependencies:
|
||||
after "0.8.2"
|
||||
arraybuffer.slice "~0.0.7"
|
||||
base64-arraybuffer "0.1.4"
|
||||
blob "0.0.5"
|
||||
has-binary2 "~1.0.2"
|
||||
engine.io-parser@~5.0.3:
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.7.tgz#ed5eae76c71f398284c578ab6deafd3ba7e4e4f6"
|
||||
integrity sha512-P+jDFbvK6lE3n1OL+q9KuzdOFWkkZ/cMV9gol/SbVfpyqfvrfrFTOFJ6fQm2VC3PZHlU3QPhVwmbsCnauHF2MQ==
|
||||
|
||||
enhanced-resolve@^5.10.0:
|
||||
version "5.12.0"
|
||||
@ -5915,18 +5856,6 @@ has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
|
||||
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
|
||||
|
||||
has-binary2@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
|
||||
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
|
||||
dependencies:
|
||||
isarray "2.0.1"
|
||||
|
||||
has-cors@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
|
||||
integrity sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
@ -6251,11 +6180,6 @@ indent-string@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
|
||||
|
||||
indexof@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||
integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
@ -6539,11 +6463,6 @@ is-wsl@^2.2.0:
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
isarray@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
|
||||
integrity sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==
|
||||
|
||||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
@ -8084,16 +8003,6 @@ parse5@6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
parseqs@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
|
||||
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
|
||||
|
||||
parseuri@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
|
||||
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
|
||||
|
||||
parseurl@~1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
@ -9767,31 +9676,23 @@ sliced@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
|
||||
integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==
|
||||
|
||||
socket.io-client@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.1.tgz#91a4038ef4d03c19967bb3c646fec6e0eaa78cff"
|
||||
integrity sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==
|
||||
socket.io-client@4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab"
|
||||
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
|
||||
dependencies:
|
||||
backo2 "1.0.2"
|
||||
component-bind "1.0.0"
|
||||
component-emitter "~1.3.0"
|
||||
debug "~3.1.0"
|
||||
engine.io-client "~3.4.0"
|
||||
has-binary2 "~1.0.2"
|
||||
indexof "0.0.1"
|
||||
parseqs "0.0.6"
|
||||
parseuri "0.0.6"
|
||||
socket.io-parser "~3.3.0"
|
||||
to-array "0.1.4"
|
||||
"@socket.io/component-emitter" "~3.1.0"
|
||||
debug "~4.3.2"
|
||||
engine.io-client "~6.4.0"
|
||||
socket.io-parser "~4.2.1"
|
||||
|
||||
socket.io-parser@~3.3.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
|
||||
integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
|
||||
socket.io-parser@~4.2.1:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667"
|
||||
integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==
|
||||
dependencies:
|
||||
component-emitter "~1.3.0"
|
||||
debug "~3.1.0"
|
||||
isarray "2.0.1"
|
||||
"@socket.io/component-emitter" "~3.1.0"
|
||||
debug "~4.3.1"
|
||||
|
||||
sockjs@^0.3.24:
|
||||
version "0.3.24"
|
||||
@ -10267,7 +10168,7 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5:
|
||||
serialize-javascript "^6.0.1"
|
||||
terser "^5.16.5"
|
||||
|
||||
terser@^5.0.0, terser@^5.10.0:
|
||||
terser@^5.0.0, terser@^5.10.0, terser@^5.16.5:
|
||||
version "5.16.9"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.9.tgz#7a28cb178e330c484369886f2afd623d9847495f"
|
||||
integrity sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==
|
||||
@ -10277,16 +10178,6 @@ terser@^5.0.0, terser@^5.10.0:
|
||||
commander "^2.20.0"
|
||||
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:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
||||
@ -10335,11 +10226,6 @@ tmpl@1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
|
||||
|
||||
to-array@0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
|
||||
integrity sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==
|
||||
|
||||
to-fast-properties@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||
@ -11163,12 +11049,10 @@ ws@^8.13.0:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
|
||||
ws@~6.1.0:
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
|
||||
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
ws@~8.11.0:
|
||||
version "8.11.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
|
||||
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
|
||||
|
||||
xml-name-validator@^3.0.0:
|
||||
version "3.0.0"
|
||||
@ -11180,10 +11064,10 @@ xmlchars@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xmlhttprequest-ssl@~1.5.4:
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
|
||||
integrity sha512-/bFPLUgJrfGUL10AIv4Y7/CUt6so9CLtB/oFxQSHseSDNNCdC6vwwKEqwLN6wNPBg9YWXAiMu8jkf6RPRS/75Q==
|
||||
xmlhttprequest-ssl@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
||||
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
|
||||
|
||||
xmlhttprequest@1.8.0:
|
||||
version "1.8.0"
|
||||
@ -11228,11 +11112,6 @@ yargs@^16.2.0:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
integrity sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
Reference in New Issue
Block a user