Compare commits

..

3 Commits

Author SHA1 Message Date
Ryan Di
c0efc16270 fix: do not bind invisible part of an element to arrow 2023-07-19 18:15:04 +08:00
David Luzar
9f76f8677b feat: cache most of element selection (#6747) 2023-07-17 01:09:44 +02:00
David Luzar
2e46e27490 fix: use actual dock state to not close docked library on insert (#6766) 2023-07-14 20:21:02 +02:00
42 changed files with 713 additions and 4153 deletions

View File

@@ -54,7 +54,7 @@
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "4.6.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2", "tunnel-rat": "0.1.2",
"workbox-background-sync": "^6.5.4", "workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4", "workbox-broadcast-update": "^6.5.4",

View File

@@ -1,6 +1,4 @@
import { register } from "./register"; import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement"; import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random"; import { randomId } from "../random";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -9,14 +7,11 @@ export const actionAddToLibrary = register({
name: "addToLibrary", name: "addToLibrary",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
if (selectedElements.some((element) => element.type === "image")) { if (selectedElements.some((element) => element.type === "image")) {
return { return {
commitToHistory: false, commitToHistory: false,

View File

@@ -13,19 +13,18 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const alignActionsPredicate = ( const alignActionsPredicate = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
_: unknown,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return ( return (
selectedElements.length > 1 && selectedElements.length > 1 &&
// TODO enable aligning frames when implemented properly // TODO enable aligning frames when implemented properly
@@ -36,12 +35,10 @@ const alignActionsPredicate = (
const alignSelectedElements = ( const alignSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
alignment: Alignment, alignment: Alignment,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
const updatedElements = alignElements(selectedElements, alignment); const updatedElements = alignElements(selectedElements, alignment);
@@ -50,6 +47,7 @@ const alignSelectedElements = (
return updateFrameMembershipOfSelectedElements( return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element), elements.map((element) => updatedElementsMap.get(element.id) || element),
appState, appState,
app,
); );
}; };
@@ -57,10 +55,10 @@ export const actionAlignTop = register({
name: "alignTop", name: "alignTop",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "start", position: "start",
axis: "y", axis: "y",
}), }),
@@ -69,9 +67,9 @@ export const actionAlignTop = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignTopIcon} icon={AlignTopIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -88,10 +86,10 @@ export const actionAlignBottom = register({
name: "alignBottom", name: "alignBottom",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "end", position: "end",
axis: "y", axis: "y",
}), }),
@@ -100,9 +98,9 @@ export const actionAlignBottom = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignBottomIcon} icon={AlignBottomIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -119,10 +117,10 @@ export const actionAlignLeft = register({
name: "alignLeft", name: "alignLeft",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "start", position: "start",
axis: "x", axis: "x",
}), }),
@@ -131,9 +129,9 @@ export const actionAlignLeft = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignLeftIcon} icon={AlignLeftIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -150,10 +148,10 @@ export const actionAlignRight = register({
name: "alignRight", name: "alignRight",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "end", position: "end",
axis: "x", axis: "x",
}), }),
@@ -162,9 +160,9 @@ export const actionAlignRight = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignRightIcon} icon={AlignRightIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -181,19 +179,19 @@ export const actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered", name: "alignVerticallyCentered",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "center", position: "center",
axis: "y", axis: "y",
}), }),
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={CenterVerticallyIcon} icon={CenterVerticallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -208,19 +206,19 @@ export const actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered", name: "alignHorizontallyCentered",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "center", position: "center",
axis: "x", axis: "x",
}), }),
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={CenterHorizontallyIcon} icon={CenterHorizontallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}

View File

@@ -4,7 +4,7 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
TEXT_ALIGN, TEXT_ALIGN,
} from "../constants"; } from "../constants";
import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { import {
computeBoundTextPosition, computeBoundTextPosition,
@@ -29,7 +29,6 @@ import {
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@@ -39,16 +38,13 @@ export const actionUnbindText = register({
name: "unbindText", name: "unbindText",
contextItemLabel: "labels.unbindText", contextItemLabel: "labels.unbindText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.some((element) => hasBoundTextElement(element)); return selectedElements.some((element) => hasBoundTextElement(element));
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
@@ -93,8 +89,8 @@ export const actionBindText = register({
name: "bindText", name: "bindText",
contextItemLabel: "labels.bindText", contextItemLabel: "labels.bindText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 2) { if (selectedElements.length === 2) {
const textElement = const textElement =
@@ -117,11 +113,8 @@ export const actionBindText = register({
} }
return false; return false;
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
let textElement: ExcalidrawTextElement; let textElement: ExcalidrawTextElement;
let container: ExcalidrawTextContainer; let container: ExcalidrawTextContainer;
@@ -201,16 +194,13 @@ export const actionWrapTextInContainer = register({
name: "wrapTextInContainer", name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText", contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
const areTextElements = selectedElements.every((el) => isTextElement(el)); const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements; return selectedElements.length > 0 && areTextElements;
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice(); let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {}; const containerIds: Mutable<AppState["selectedElementIds"]> = {};

View File

@@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene"; import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
@@ -302,11 +302,8 @@ export const zoomToFit = ({
export const actionZoomToFitSelectionInViewport = register({ export const actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport", name: "zoomToFitSelectionInViewport",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return zoomToFit({ return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements, targetElements: selectedElements.length ? selectedElements : elements,
appState, appState,
@@ -325,11 +322,8 @@ export const actionZoomToFitSelectionInViewport = register({
export const actionZoomToFitSelection = register({ export const actionZoomToFitSelection = register({
name: "zoomToFitSelection", name: "zoomToFitSelection",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return zoomToFit({ return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements, targetElements: selectedElements.length ? selectedElements : elements,
appState, appState,

View File

@@ -7,7 +7,6 @@ import {
probablySupportsClipboardWriteText, probablySupportsClipboardWriteText,
} from "../clipboard"; } from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected"; import { actionDeleteSelected } from "./actionDeleteSelected";
import { getSelectedElements } from "../scene/selection";
import { exportCanvas } from "../data/index"; import { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -16,7 +15,8 @@ export const actionCopy = register({
name: "copy", name: "copy",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const elementsToCopy = getSelectedElements(elements, appState, { const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}); });
@@ -75,14 +75,11 @@ export const actionCopyAsSvg = register({
commitToHistory: false, commitToHistory: false,
}; };
} }
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
try { try {
await exportCanvas( await exportCanvas(
"clipboard-svg", "clipboard-svg",
@@ -122,14 +119,11 @@ export const actionCopyAsPng = register({
commitToHistory: false, commitToHistory: false,
}; };
} }
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
try { try {
await exportCanvas( await exportCanvas(
"clipboard", "clipboard",
@@ -177,14 +171,11 @@ export const actionCopyAsPng = register({
export const copyText = register({ export const copyText = register({
name: "copyText", name: "copyText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
const text = selectedElements const text = selectedElements
.reduce((acc: string[], element) => { .reduce((acc: string[], element) => {
@@ -199,12 +190,15 @@ export const copyText = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
return ( return (
probablySupportsClipboardWriteText && probablySupportsClipboardWriteText &&
getSelectedElements(elements, appState, { app.scene
.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
}).some(isTextElement) })
.some(isTextElement)
); );
}, },
contextItemLabel: "labels.copyText", contextItemLabel: "labels.copyText",

View File

@@ -9,19 +9,13 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
elements: readonly ExcalidrawElement[], const selectedElements = app.scene.getSelectedElements(appState);
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return ( return (
selectedElements.length > 1 && selectedElements.length > 1 &&
// TODO enable distributing frames when implemented properly // TODO enable distributing frames when implemented properly
@@ -32,12 +26,10 @@ const enableActionGroup = (
const distributeSelectedElements = ( const distributeSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
distribution: Distribution, distribution: Distribution,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
const updatedElements = distributeElements(selectedElements, distribution); const updatedElements = distributeElements(selectedElements, distribution);
@@ -46,16 +38,17 @@ const distributeSelectedElements = (
return updateFrameMembershipOfSelectedElements( return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element), elements.map((element) => updatedElementsMap.get(element.id) || element),
appState, appState,
app,
); );
}; };
export const distributeHorizontally = register({ export const distributeHorizontally = register({
name: "distributeHorizontally", name: "distributeHorizontally",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: distributeSelectedElements(elements, appState, { elements: distributeSelectedElements(elements, appState, app, {
space: "between", space: "between",
axis: "x", axis: "x",
}), }),
@@ -64,9 +57,9 @@ export const distributeHorizontally = register({
}, },
keyTest: (event) => keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H, !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(appState, app)}
type="button" type="button"
icon={DistributeHorizontallyIcon} icon={DistributeHorizontallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -82,10 +75,10 @@ export const distributeHorizontally = register({
export const distributeVertically = register({ export const distributeVertically = register({
name: "distributeVertically", name: "distributeVertically",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: distributeSelectedElements(elements, appState, { elements: distributeSelectedElements(elements, appState, app, {
space: "between", space: "between",
axis: "y", axis: "y",
}), }),
@@ -94,9 +87,9 @@ export const distributeVertically = register({
}, },
keyTest: (event) => keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V, !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(appState, app)}
type="button" type="button"
icon={DistributeVerticallyIcon} icon={DistributeVerticallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}

View File

@@ -275,6 +275,7 @@ const duplicateElements = (
}, },
getNonDeletedElements(finalElements), getNonDeletedElements(finalElements),
appState, appState,
null,
), ),
}; };
}; };

View File

@@ -1,7 +1,6 @@
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { register } from "./register"; import { register } from "./register";
@@ -11,14 +10,15 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
export const actionToggleElementLock = register({ export const actionToggleElementLock = register({
name: "toggleElementLock", name: "toggleElementLock",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
return !selectedElements.some( return !selectedElements.some(
(element) => element.locked && element.frameId, (element) => element.locked && element.frameId,
); );
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, { const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}); });
@@ -46,8 +46,9 @@ export const actionToggleElementLock = register({
commitToHistory: true, commitToHistory: true,
}; };
}, },
contextItemLabel: (elements, appState) => { contextItemLabel: (elements, appState, app) => {
const selected = getSelectedElements(elements, appState, { const selected = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false, includeBoundTextElement: false,
}); });
if (selected.length === 1 && selected[0].type !== "frame") { if (selected.length === 1 && selected[0].type !== "frame") {
@@ -60,12 +61,13 @@ export const actionToggleElementLock = register({
? "labels.elementLock.lockAll" ? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll"; : "labels.elementLock.unlockAll";
}, },
keyTest: (event, appState, elements) => { keyTest: (event, appState, elements, app) => {
return ( return (
event.key.toLocaleLowerCase() === KEYS.L && event.key.toLocaleLowerCase() === KEYS.L &&
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
event.shiftKey && event.shiftKey &&
getSelectedElements(elements, appState, { app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false, includeBoundTextElement: false,
}).length > 0 }).length > 0
); );

View File

@@ -17,11 +17,12 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
export const actionFlipHorizontal = register({ export const actionFlipHorizontal = register({
name: "flipHorizontal", name: "flipHorizontal",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
elements: updateFrameMembershipOfSelectedElements( elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "horizontal"), flipSelectedElements(elements, appState, "horizontal"),
appState, appState,
app,
), ),
appState, appState,
commitToHistory: true, commitToHistory: true,
@@ -34,11 +35,12 @@ export const actionFlipHorizontal = register({
export const actionFlipVertical = register({ export const actionFlipVertical = register({
name: "flipVertical", name: "flipVertical",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
elements: updateFrameMembershipOfSelectedElements( elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "vertical"), flipSelectedElements(elements, appState, "vertical"),
appState, appState,
app,
), ),
appState, appState,
commitToHistory: true, commitToHistory: true,

View File

@@ -3,19 +3,12 @@ import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame"; import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame"; import { getFrameElements } from "../frame";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene"; import { AppClassProperties, AppState } from "../types";
import { AppState } from "../types";
import { setCursorForShape, updateActiveTool } from "../utils"; import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
const isSingleFrameSelected = ( const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
elements: readonly ExcalidrawElement[], const selectedElements = app.scene.getSelectedElements(appState);
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return selectedElements.length === 1 && selectedElements[0].type === "frame"; return selectedElements.length === 1 && selectedElements[0].type === "frame";
}; };
@@ -23,11 +16,8 @@ const isSingleFrameSelected = (
export const actionSelectAllElementsInFrame = register({ export const actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame", name: "selectAllElementsInFrame",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedFrame = getSelectedElements( const selectedFrame = app.scene.getSelectedElements(appState)[0];
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") { if (selectedFrame && selectedFrame.type === "frame") {
const elementsInFrame = getFrameElements( const elementsInFrame = getFrameElements(
@@ -55,17 +45,15 @@ export const actionSelectAllElementsInFrame = register({
}; };
}, },
contextItemLabel: "labels.selectAllElementsInFrame", contextItemLabel: "labels.selectAllElementsInFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState), predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
}); });
export const actionRemoveAllElementsFromFrame = register({ export const actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame", name: "removeAllElementsFromFrame",
trackEvent: { category: "history" }, trackEvent: { category: "history" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedFrame = getSelectedElements( const selectedFrame = app.scene.getSelectedElements(appState)[0];
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") { if (selectedFrame && selectedFrame.type === "frame") {
return { return {
@@ -87,7 +75,8 @@ export const actionRemoveAllElementsFromFrame = register({
}; };
}, },
contextItemLabel: "labels.removeAllElementsFromFrame", contextItemLabel: "labels.removeAllElementsFromFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState), predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
}); });
export const actionupdateFrameRendering = register({ export const actionupdateFrameRendering = register({

View File

@@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons"; import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { import {
getSelectedGroupIds, getSelectedGroupIds,
selectGroup, selectGroup,
@@ -22,7 +22,7 @@ import {
ExcalidrawFrameElement, ExcalidrawFrameElement,
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { import {
getElementsInResizingFrame, getElementsInResizingFrame,
@@ -51,14 +51,12 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
const enableActionGroup = ( const enableActionGroup = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
return ( return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements) selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
); );
@@ -68,13 +66,10 @@ export const actionGroup = register({
name: "group", name: "group",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
if (selectedElements.length < 2) { if (selectedElements.length < 2) {
// nothing to group // nothing to group
return { appState, elements, commitToHistory: false }; return { appState, elements, commitToHistory: false };
@@ -164,12 +159,13 @@ export const actionGroup = register({
}; };
}, },
contextItemLabel: "labels.group", contextItemLabel: "labels.group",
predicate: (elements, appState) => enableActionGroup(elements, appState), predicate: (elements, appState, _, app) =>
enableActionGroup(elements, appState, app),
keyTest: (event) => keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G, !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState, app)}
type="button" type="button"
icon={<GroupIcon theme={appState.theme} />} icon={<GroupIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@@ -191,7 +187,7 @@ export const actionUngroup = register({
let nextElements = [...elements]; let nextElements = [...elements];
const selectedElements = getSelectedElements(nextElements, appState); const selectedElements = app.scene.getSelectedElements(appState);
const frames = selectedElements const frames = selectedElements
.filter((element) => element.frameId) .filter((element) => element.frameId)
.map((element) => .map((element) =>
@@ -219,6 +215,7 @@ export const actionUngroup = register({
{ ...appState, selectedGroupIds: {} }, { ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements), getNonDeletedElements(nextElements),
appState, appState,
null,
); );
frames.forEach((frame) => { frames.forEach((frame) => {

View File

@@ -1,8 +1,6 @@
import { getNonDeletedElements } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types"; import { ExcalidrawLinearElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { register } from "./register"; import { register } from "./register";
export const actionToggleLinearEditor = register({ export const actionToggleLinearEditor = register({
@@ -10,21 +8,18 @@ export const actionToggleLinearEditor = register({
trackEvent: { trackEvent: {
category: "element", category: "element",
}, },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true; return true;
} }
return false; return false;
}, },
perform(elements, appState, _, app) { perform(elements, appState, _, app) {
const selectedElement = getSelectedElements( const selectedElement = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, })[0] as ExcalidrawLinearElement;
)[0] as ExcalidrawLinearElement;
const editingLinearElement = const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id appState.editingLinearElement?.elementId === selectedElement.id
@@ -38,14 +33,11 @@ export const actionToggleLinearEditor = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
contextItemLabel: (elements, appState) => { contextItemLabel: (elements, appState, app) => {
const selectedElement = getSelectedElements( const selectedElement = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, })[0] as ExcalidrawLinearElement;
)[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement.id return appState.editingLinearElement?.elementId === selectedElement.id
? "labels.lineEditor.exit" ? "labels.lineEditor.exit"
: "labels.lineEditor.edit"; : "labels.lineEditor.edit";

View File

@@ -42,6 +42,7 @@ export const actionSelectAll = register({
}, },
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
app,
), ),
commitToHistory: true, commitToHistory: true,
}; };

View File

@@ -90,6 +90,7 @@ export class ActionManager {
event, event,
this.getAppState(), this.getAppState(),
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.app,
), ),
); );
@@ -168,6 +169,7 @@ export class ActionManager {
appState={this.getAppState()} appState={this.getAppState()}
updateData={updateData} updateData={updateData}
appProps={this.app.props} appProps={this.app.props}
app={this.app}
data={data} data={data}
/> />
); );

View File

@@ -130,6 +130,7 @@ export type PanelComponentProps = {
updateData: (formData?: any) => void; updateData: (formData?: any) => void;
appProps: ExcalidrawProps; appProps: ExcalidrawProps;
data?: Record<string, any>; data?: Record<string, any>;
app: AppClassProperties;
}; };
export interface Action { export interface Action {
@@ -141,12 +142,14 @@ export interface Action {
event: React.KeyboardEvent | KeyboardEvent, event: React.KeyboardEvent | KeyboardEvent,
appState: AppState, appState: AppState,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
app: AppClassProperties,
) => boolean; ) => boolean;
contextItemLabel?: contextItemLabel?:
| string | string
| (( | ((
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
) => string); ) => string);
predicate?: ( predicate?: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],

View File

@@ -330,6 +330,7 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper"; import { activeEyeDropperAtom } from "./EyeDropper";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -473,8 +474,6 @@ class App extends React.Component<AppProps, AppState> {
name, name,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
showHyperlinkPopup: false,
defaultSidebarDockedPreference: false,
}; };
this.id = nanoid(); this.id = nanoid();
@@ -799,10 +798,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
public render() { public render() {
const selectedElement = getSelectedElements( const selectedElement = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const { renderTopRightUI, renderCustomStats } = this.props; const { renderTopRightUI, renderCustomStats } = this.props;
return ( return (
@@ -859,6 +855,7 @@ class App extends React.Component<AppProps, AppState> {
!this.state.zenModeEnabled && !this.state.zenModeEnabled &&
!this.scene.getElementsIncludingDeleted().length !this.scene.getElementsIncludingDeleted().length
} }
app={this}
> >
{this.props.children} {this.props.children}
</LayerUI> </LayerUI>
@@ -964,10 +961,7 @@ class App extends React.Component<AppProps, AppState> {
const shouldUpdateStrokeColor = const shouldUpdateStrokeColor =
(type === "background" && event.altKey) || (type === "background" && event.altKey) ||
(type === "stroke" && !event.altKey); (type === "stroke" && !event.altKey);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getElementsIncludingDeleted(),
this.state,
);
if ( if (
!selectedElements.length || !selectedElements.length ||
this.state.activeTool.type !== "selection" this.state.activeTool.type !== "selection"
@@ -2031,7 +2025,7 @@ class App extends React.Component<AppProps, AppState> {
openSidebar: openSidebar:
this.state.openSidebar && this.state.openSidebar &&
this.device.canDeviceFitSidebar && this.device.canDeviceFitSidebar &&
this.state.defaultSidebarDockedPreference jotaiStore.get(isSidebarDockedAtom)
? this.state.openSidebar ? this.state.openSidebar
: null, : null,
selectedElementIds: nextElementsToSelect.reduce( selectedElementIds: nextElementsToSelect.reduce(
@@ -2047,6 +2041,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
this,
), ),
() => { () => {
if (opts.files) { if (opts.files) {
@@ -2377,7 +2372,6 @@ class App extends React.Component<AppProps, AppState> {
toast: { toast: {
message: string; message: string;
closable?: boolean; closable?: boolean;
spinner?: boolean;
duration?: number; duration?: number;
} | null, } | null,
) => { ) => {
@@ -2612,14 +2606,11 @@ class App extends React.Component<AppProps, AppState> {
offsetY = step; offsetY = step;
} }
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements({
this.scene.getNonDeletedElements(), selectedElementIds: this.state.selectedElementIds,
this.state,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
mutateElement(element, { mutateElement(element, {
@@ -2636,10 +2627,7 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ENTER) { } else if (event.key === KEYS.ENTER) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const selectedElement = selectedElements[0]; const selectedElement = selectedElements[0];
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
@@ -2715,10 +2703,7 @@ class App extends React.Component<AppProps, AppState> {
!event.altKey && !event.altKey &&
!event[KEYS.CTRL_OR_CMD] !event[KEYS.CTRL_OR_CMD]
) { ) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if ( if (
this.state.activeTool.type === "selection" && this.state.activeTool.type === "selection" &&
!selectedElements.length !selectedElements.length
@@ -2790,10 +2775,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true }); this.setState({ isBindingEnabled: true });
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
isBindingEnabled(this.state) isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements) ? bindOrUnbindSelectedElements(selectedElements)
: unbindLinearElements(selectedElements); : unbindLinearElements(selectedElements);
@@ -3143,10 +3125,7 @@ class App extends React.Component<AppProps, AppState> {
} }
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null; let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) { if (isTextElement(selectedElements[0])) {
@@ -3276,10 +3255,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if ( if (
@@ -3330,6 +3306,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
); );
return; return;
@@ -3706,7 +3683,7 @@ class App extends React.Component<AppProps, AppState> {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
!isOverScrollBar && !isOverScrollBar &&
@@ -4409,10 +4386,7 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLElement>, event: React.PointerEvent<HTMLElement>,
): PointerDownState { ): PointerDownState {
const origin = viewportCoordsToSceneCoords(event, this.state); const origin = viewportCoordsToSceneCoords(event, this.state);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
return { return {
@@ -4530,7 +4504,7 @@ class App extends React.Component<AppProps, AppState> {
): boolean => { ): boolean => {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
getElementWithTransformHandleType( getElementWithTransformHandleType(
@@ -4773,6 +4747,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
@@ -5200,7 +5175,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.offset === null) { if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors( pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY( getDragOffsetXY(
getSelectedElements(this.scene.getNonDeletedElements(), this.state), this.scene.getSelectedElements(this.state),
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@@ -5363,10 +5338,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
!isSelectingPointsInLineEditor !isSelectingPointsInLineEditor
) { ) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.every((element) => element.locked)) { if (selectedElements.every((element) => element.locked)) {
return; return;
@@ -5437,14 +5409,18 @@ class App extends React.Component<AppProps, AppState> {
const groupIdMap = new Map(); const groupIdMap = new Map();
const oldIdToDuplicatedId = new Map(); const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element; const hitElement = pointerDownState.hit.element;
const elements = this.scene.getElementsIncludingDeleted();
const selectedElementIds = new Set( const selectedElementIds = new Set(
getSelectedElements(elements, this.state, { this.scene
.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}).map((element) => element.id), })
.map((element) => element.id),
); );
const elements = this.scene.getNonDeletedElements();
for (const element of elements) { for (const element of elements) {
if ( if (
selectedElementIds.has(element.id) || selectedElementIds.has(element.id) ||
@@ -5586,6 +5562,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
); );
} }
@@ -5643,6 +5620,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} }
@@ -5742,10 +5720,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit?.element?.id !== pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId this.state.selectedLinearElement.elementId
) { ) {
const selectedELements = getSelectedElements( const selectedELements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles // set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
if (selectedELements.length > 1) { if (selectedELements.length > 1) {
this.setState({ selectedLinearElement: null }); this.setState({ selectedLinearElement: null });
@@ -5987,10 +5962,7 @@ class App extends React.Component<AppProps, AppState> {
const topLayerFrame = const topLayerFrame =
this.getTopLayerFrameAtSceneCoords(sceneCoords); this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
let nextElements = this.scene.getElementsIncludingDeleted(); let nextElements = this.scene.getElementsIncludingDeleted();
const updateGroupIdsAfterEditingGroup = ( const updateGroupIdsAfterEditingGroup = (
@@ -6069,6 +6041,7 @@ class App extends React.Component<AppProps, AppState> {
nextElements = updateFrameMembershipOfSelectedElements( nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
this.state, this.state,
this,
); );
this.scene.replaceAllElements(nextElements); this.scene.replaceAllElements(nextElements);
@@ -6113,12 +6086,12 @@ class App extends React.Component<AppProps, AppState> {
let nextElements = updateFrameMembershipOfSelectedElements( let nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
this.state, this.state,
this,
); );
const selectedFrames = getSelectedElements( const selectedFrames = this.scene
this.scene.getElementsIncludingDeleted(), .getSelectedElements(this.state)
this.state, .filter(
).filter(
(element) => element.type === "frame", (element) => element.type === "frame",
) as ExcalidrawFrameElement[]; ) as ExcalidrawFrameElement[];
@@ -6145,10 +6118,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.selectedLinearElement?.elementId !== hitElement?.id && this.state.selectedLinearElement?.elementId !== hitElement?.id &&
isLinearElement(hitElement) isLinearElement(hitElement)
) { ) {
const selectedELements = getSelectedElements( const selectedELements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement when no other element selected except // set selectedLinearElement when no other element selected except
// the one we've hit // the one we've hit
if (selectedELements.length === 1) { if (selectedELements.length === 1) {
@@ -6250,7 +6220,7 @@ class App extends React.Component<AppProps, AppState> {
delete newSelectedElementIds[hitElement!.id]; delete newSelectedElementIds[hitElement!.id];
const newSelectedElements = getSelectedElements( const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
{ ...prevState, selectedElementIds: newSelectedElementIds }, { selectedElementIds: newSelectedElementIds },
); );
return selectGroupsForSelectedElements( return selectGroupsForSelectedElements(
@@ -6269,6 +6239,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} }
@@ -6305,6 +6276,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} else { } else {
@@ -6335,6 +6307,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
})); }));
} }
@@ -6394,9 +6367,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
(isBindingEnabled(this.state) (isBindingEnabled(this.state)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements
: unbindLinearElements)( : unbindLinearElements)(this.scene.getSelectedElements(this.state));
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
);
} }
if (!activeTool.locked && activeTool.type !== "freedraw") { if (!activeTool.locked && activeTool.type !== "freedraw") {
@@ -7103,10 +7074,7 @@ class App extends React.Component<AppProps, AppState> {
includeLockedElements: true, includeLockedElements: true,
}); });
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const isHittignCommonBoundBox = const isHittignCommonBoundBox =
this.isHittingCommonBoundingBoxOfSelectedElements( this.isHittingCommonBoundingBoxOfSelectedElements(
{ x, y }, { x, y },
@@ -7136,6 +7104,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
this,
) )
: this.state), : this.state),
showHyperlinkPopup: false, showHyperlinkPopup: false,
@@ -7223,10 +7192,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent, event: MouseEvent | KeyboardEvent,
): boolean => { ): boolean => {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const selectedFrames = selectedElements.filter( const selectedFrames = selectedElements.filter(
(element) => element.type === "frame", (element) => element.type === "frame",
) as ExcalidrawFrameElement[]; ) as ExcalidrawFrameElement[];

View File

@@ -82,7 +82,9 @@ export const ContextMenu = React.memo(
let label = ""; let label = "";
if (item.contextItemLabel) { if (item.contextItemLabel) {
if (typeof item.contextItemLabel === "function") { if (typeof item.contextItemLabel === "function") {
label = t(item.contextItemLabel(elements, appState)); label = t(
item.contextItemLabel(elements, appState, actionManager.app),
);
} else { } else {
label = t(item.contextItemLabel); label = t(item.contextItemLabel);
} }

View File

@@ -1,7 +1,5 @@
import { t } from "../i18n"; import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types"; import { AppClassProperties, Device, UIAppState } from "../types";
import { getSelectedElements } from "../scene";
import { Device, UIAppState } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
@@ -15,17 +13,12 @@ import "./HintViewer.scss";
interface HintViewerProps { interface HintViewerProps {
appState: UIAppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean; isMobile: boolean;
device: Device; device: Device;
app: AppClassProperties;
} }
const getHints = ({ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
appState,
elements,
isMobile,
device,
}: HintViewerProps) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
@@ -55,7 +48,7 @@ const getHints = ({
return t("hints.placeImage"); return t("hints.placeImage");
} }
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if ( if (
isResizing && isResizing &&
@@ -115,15 +108,15 @@ const getHints = ({
export const HintViewer = ({ export const HintViewer = ({
appState, appState,
elements,
isMobile, isMobile,
device, device,
app,
}: HintViewerProps) => { }: HintViewerProps) => {
let hint = getHints({ let hint = getHints({
appState, appState,
elements,
isMobile, isMobile,
device, device,
app,
}); });
if (!hint) { if (!hint) {
return null; return null;

View File

@@ -72,6 +72,7 @@ interface LayerUIProps {
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
app: AppClassProperties;
} }
const DefaultMainMenu: React.FC<{ const DefaultMainMenu: React.FC<{
@@ -127,6 +128,7 @@ const LayerUI = ({
onExportImage, onExportImage,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
app,
}: LayerUIProps) => { }: LayerUIProps) => {
const device = useDevice(); const device = useDevice();
const tunnels = useInitializeTunnels(); const tunnels = useInitializeTunnels();
@@ -240,9 +242,9 @@ const LayerUI = ({
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
elements={elements}
isMobile={device.isMobile} isMobile={device.isMobile}
device={device} device={device}
app={app}
/> />
{heading} {heading}
<Stack.Row gap={1}> <Stack.Row gap={1}>
@@ -401,6 +403,7 @@ const LayerUI = ({
)} )}
{device.isMobile && ( {device.isMobile && (
<MobileMenu <MobileMenu
app={app}
appState={appState} appState={appState}
elements={elements} elements={elements}
actionManager={actionManager} actionManager={actionManager}

View File

@@ -1,5 +1,11 @@
import React from "react"; import React from "react";
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types"; import {
AppClassProperties,
AppState,
Device,
ExcalidrawProps,
UIAppState,
} from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@@ -41,6 +47,7 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
device: Device; device: Device;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
app: AppClassProperties;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -58,6 +65,7 @@ export const MobileMenu = ({
renderSidebars, renderSidebars,
device, device,
renderWelcomeScreen, renderWelcomeScreen,
app,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const { const {
WelcomeScreenCenterTunnel, WelcomeScreenCenterTunnel,
@@ -119,9 +127,9 @@ export const MobileMenu = ({
</Section> </Section>
<HintViewer <HintViewer
appState={appState} appState={appState}
elements={elements}
isMobile={true} isMobile={true}
device={device} device={device}
app={app}
/> />
</FixedSideContainer> </FixedSideContainer>
); );

View File

@@ -25,17 +25,6 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.Toast__message--spinner {
padding: 0 3rem;
}
.Toast__spinner {
position: absolute;
left: 1.5rem;
top: 50%;
margin-top: -8px;
}
.close { .close {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@@ -1,7 +1,5 @@
import clsx from "clsx";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { CloseIcon } from "./icons"; import { CloseIcon } from "./icons";
import Spinner from "./Spinner";
import "./Toast.scss"; import "./Toast.scss";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@@ -11,14 +9,12 @@ export const Toast = ({
message, message,
onClose, onClose,
closable = false, closable = false,
spinner = true,
// To prevent autoclose, pass duration as Infinity // To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT, duration = DEFAULT_TOAST_TIMEOUT,
}: { }: {
message: string; message: string;
onClose: () => void; onClose: () => void;
closable?: boolean; closable?: boolean;
spinner?: boolean;
duration?: number; duration?: number;
}) => { }) => {
const timerRef = useRef<number>(0); const timerRef = useRef<number>(0);
@@ -48,18 +44,7 @@ export const Toast = ({
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
{spinner && ( <p className="Toast__message">{message}</p>
<div className="Toast__spinner">
<Spinner />
</div>
)}
<p
className={clsx("Toast__message", {
"Toast__message--spinner": spinner,
})}
>
{message}
</p>
{closable && ( {closable && (
<ToolButton <ToolButton
icon={CloseIcon} icon={CloseIcon}

View File

@@ -27,6 +27,7 @@ import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils"; import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getContainingFrame, isPointInFrame } from "../frame";
export type SuggestedBinding = export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement> | NonDeleted<ExcalidrawBindableElement>
@@ -274,6 +275,18 @@ export const getHoveredElementForBinding = (
isBindableElement(element, false) && isBindableElement(element, false) &&
bindingBorderTest(element, pointerCoords), bindingBorderTest(element, pointerCoords),
); );
if (hoveredElement) {
const frame = getContainingFrame(hoveredElement);
if (frame) {
if (isPointInFrame(pointerCoords, frame)) {
return hoveredElement as NonDeleted<ExcalidrawBindableElement>;
}
return null;
}
}
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null; return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
}; };
@@ -499,10 +512,22 @@ const getElligibleElementsForBindingElement = (
return [ return [
getElligibleElementForBindingElement(linearElement, "start"), getElligibleElementForBindingElement(linearElement, "start"),
getElligibleElementForBindingElement(linearElement, "end"), getElligibleElementForBindingElement(linearElement, "end"),
].filter( ].filter((element): element is NonDeleted<ExcalidrawBindableElement> => {
(element): element is NonDeleted<ExcalidrawBindableElement> => if (element != null) {
element != null, const frame = getContainingFrame(element);
); return frame
? isPointInFrame(
getLinearElementEdgeCoors(linearElement, "start"),
frame,
) ||
isPointInFrame(
getLinearElementEdgeCoors(linearElement, "end"),
frame,
)
: true;
}
return false;
});
}; };
const getElligibleElementForBindingElement = ( const getElligibleElementForBindingElement = (

View File

@@ -7,8 +7,6 @@ export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
export const SYNC_BROWSER_TABS_TIMEOUT = 50; export const SYNC_BROWSER_TABS_TIMEOUT = 50;
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day 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 export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
// 1 year (https://stackoverflow.com/a/25201898/927631) // 1 year (https://stackoverflow.com/a/25201898/927631)

View File

@@ -1,6 +1,6 @@
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { PureComponent } from "react"; import { PureComponent } from "react";
import { ExcalidrawImperativeAPI, PauseCollaborationState } from "../../types"; import { ExcalidrawImperativeAPI } from "../../types";
import { ErrorDialog } from "../../components/ErrorDialog"; import { ErrorDialog } from "../../components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../constants"; import { APP_NAME, ENV, EVENT } from "../../constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
@@ -16,7 +16,6 @@ import { Collaborator, Gesture } from "../../types";
import { import {
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
upsertMap,
withBatchedUpdates, withBatchedUpdates,
} from "../../utils"; } from "../../utils";
import { import {
@@ -25,15 +24,12 @@ import {
FIREBASE_STORAGE_PREFIXES, FIREBASE_STORAGE_PREFIXES,
INITIAL_SCENE_UPDATE_TIMEOUT, INITIAL_SCENE_UPDATE_TIMEOUT,
LOAD_IMAGES_TIMEOUT, LOAD_IMAGES_TIMEOUT,
PAUSE_COLLABORATION_TIMEOUT,
WS_SCENE_EVENT_TYPES, WS_SCENE_EVENT_TYPES,
SYNC_FULL_SCENE_INTERVAL_MS, SYNC_FULL_SCENE_INTERVAL_MS,
RESUME_FALLBACK_TIMEOUT,
} from "../app_constants"; } from "../app_constants";
import { import {
generateCollaborationLinkData, generateCollaborationLinkData,
getCollaborationLink, getCollaborationLink,
getCollaborationLinkData,
getCollabServer, getCollabServer,
getSyncableElements, getSyncableElements,
SocketUpdateDataSource, SocketUpdateDataSource,
@@ -47,8 +43,8 @@ import {
saveToFirebase, saveToFirebase,
} from "../data/firebase"; } from "../data/firebase";
import { import {
importUsernameAndIdFromLocalStorage, importUsernameFromLocalStorage,
saveUsernameAndIdToLocalStorage, saveUsernameToLocalStorage,
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal"; import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
@@ -75,19 +71,16 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData"; import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { appJotaiStore } from "../app-jotai"; import { appJotaiStore } from "../app-jotai";
import { nanoid } from "nanoid";
export const collabAPIAtom = atom<CollabAPI | null>(null); export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false); export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false); export const isCollaboratingAtom = atom(false);
export const isOfflineAtom = atom(false); export const isOfflineAtom = atom(false);
export const isCollaborationPausedAtom = atom(false);
interface CollabState { interface CollabState {
errorMessage: string; errorMessage: string;
username: string; username: string;
activeRoomLink: string; activeRoomLink: string;
userId: string;
} }
type CollabInstance = InstanceType<typeof Collab>; type CollabInstance = InstanceType<typeof Collab>;
@@ -101,7 +94,6 @@ export interface CollabAPI {
syncElements: CollabInstance["syncElements"]; syncElements: CollabInstance["syncElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void; setUsername: (username: string) => void;
isPaused: () => boolean;
} }
interface PublicProps { interface PublicProps {
@@ -116,7 +108,6 @@ class Collab extends PureComponent<Props, CollabState> {
excalidrawAPI: Props["excalidrawAPI"]; excalidrawAPI: Props["excalidrawAPI"];
activeIntervalId: number | null; activeIntervalId: number | null;
idleTimeoutId: number | null; idleTimeoutId: number | null;
pauseTimeoutId: number | null;
private socketInitializationTimer?: number; private socketInitializationTimer?: number;
private lastBroadcastedOrReceivedSceneVersion: number = -1; private lastBroadcastedOrReceivedSceneVersion: number = -1;
@@ -124,13 +115,9 @@ class Collab extends PureComponent<Props, CollabState> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const { username, userId } = importUsernameAndIdFromLocalStorage() || {};
this.state = { this.state = {
errorMessage: "", errorMessage: "",
username: username || "", username: importUsernameFromLocalStorage() || "",
userId: userId || "",
activeRoomLink: "", activeRoomLink: "",
}; };
this.portal = new Portal(this); this.portal = new Portal(this);
@@ -162,7 +149,6 @@ class Collab extends PureComponent<Props, CollabState> {
this.excalidrawAPI = props.excalidrawAPI; this.excalidrawAPI = props.excalidrawAPI;
this.activeIntervalId = null; this.activeIntervalId = null;
this.idleTimeoutId = null; this.idleTimeoutId = null;
this.pauseTimeoutId = null;
} }
componentDidMount() { componentDidMount() {
@@ -181,7 +167,6 @@ class Collab extends PureComponent<Props, CollabState> {
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
stopCollaboration: this.stopCollaboration, stopCollaboration: this.stopCollaboration,
setUsername: this.setUsername, setUsername: this.setUsername,
isPaused: this.isPaused,
}; };
appJotaiStore.set(collabAPIAtom, collabAPI); appJotaiStore.set(collabAPIAtom, collabAPI);
@@ -222,10 +207,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.clearTimeout(this.idleTimeoutId); window.clearTimeout(this.idleTimeoutId);
this.idleTimeoutId = null; this.idleTimeoutId = null;
} }
if (this.pauseTimeoutId) {
window.clearTimeout(this.pauseTimeoutId);
this.pauseTimeoutId = null;
}
} }
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
@@ -329,126 +310,6 @@ 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 }) => { private destroySocketClient = (opts?: { isUnload: boolean }) => {
this.lastBroadcastedOrReceivedSceneVersion = -1; this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close(); this.portal.close();
@@ -527,11 +388,6 @@ class Collab extends PureComponent<Props, CollabState> {
}); });
} }
if (!this.state.userId) {
const userId = nanoid();
this.onUserIdChange(userId);
}
if (this.portal.socket) { if (this.portal.socket) {
return null; return null;
} }
@@ -646,7 +502,6 @@ class Collab extends PureComponent<Props, CollabState> {
elements: reconciledElements, elements: reconciledElements,
scrollToContent: true, scrollToContent: true,
}); });
this.onPauseCollaborationChange(PauseCollaborationState.SYNCED);
} }
break; break;
} }
@@ -656,63 +511,36 @@ class Collab extends PureComponent<Props, CollabState> {
); );
break; break;
case "MOUSE_LOCATION": { case "MOUSE_LOCATION": {
const { const { pointer, button, username, selectedElementIds } =
pointer, decryptedData.payload;
button, const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
username, decryptedData.payload.socketId ||
selectedElementIds, // @ts-ignore legacy, see #2094 (#2097)
userId, decryptedData.payload.socketID;
socketId,
} = decryptedData.payload; const collaborators = new Map(this.collaborators);
const collaborators = upsertMap( const user = collaborators.get(socketId) || {}!;
userId, user.pointer = pointer;
{ user.button = button;
username, user.selectedElementIds = selectedElementIds;
pointer, user.username = username;
button, collaborators.set(socketId, user);
selectedElementIds,
socketId,
},
this.collaborators,
);
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
collaborators: new Map(collaborators), collaborators,
}); });
break; break;
} }
case "IDLE_STATUS": { case "IDLE_STATUS": {
const { userState, username, userId, socketId } = const { userState, socketId, username } = decryptedData.payload;
decryptedData.payload; const collaborators = new Map(this.collaborators);
const collaborators = upsertMap( const user = collaborators.get(socketId) || {}!;
userId, user.userState = userState;
{ user.username = username;
username,
userState,
userId,
socketId,
},
this.collaborators,
);
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
collaborators: new Map(collaborators), collaborators,
}); });
break; 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),
});
}
} }
}, },
); );
@@ -781,15 +609,6 @@ class Collab extends PureComponent<Props, CollabState> {
} else { } else {
this.portal.socketInitialized = true; 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; return null;
}; };
@@ -877,10 +696,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.clearInterval(this.activeIntervalId); window.clearInterval(this.activeIntervalId);
this.activeIntervalId = null; this.activeIntervalId = null;
} }
this.pauseTimeoutId = window.setTimeout(
() => this.onPauseCollaborationChange(PauseCollaborationState.PAUSED),
PAUSE_COLLABORATION_TIMEOUT,
);
this.onIdleStateChange(UserIdleState.AWAY); this.onIdleStateChange(UserIdleState.AWAY);
} else { } else {
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD); this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
@@ -889,11 +704,6 @@ class Collab extends PureComponent<Props, CollabState> {
ACTIVE_THRESHOLD, ACTIVE_THRESHOLD,
); );
this.onIdleStateChange(UserIdleState.ACTIVE); this.onIdleStateChange(UserIdleState.ACTIVE);
if (this.pauseTimeoutId) {
window.clearTimeout(this.pauseTimeoutId);
this.onPauseCollaborationChange(PauseCollaborationState.RESUMED);
this.pauseTimeoutId = null;
}
} }
}; };
@@ -915,12 +725,17 @@ class Collab extends PureComponent<Props, CollabState> {
}; };
setCollaborators(sockets: string[]) { setCollaborators(sockets: string[]) {
this.collaborators.forEach((value, key) => { const collaborators: InstanceType<typeof Collab>["collaborators"] =
if (value.socketId && !sockets.includes(value.socketId)) { new Map();
this.collaborators.delete(key); for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
} }
}); }
this.excalidrawAPI.updateScene({ collaborators: this.collaborators }); this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
} }
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
@@ -1006,12 +821,7 @@ class Collab extends PureComponent<Props, CollabState> {
onUsernameChange = (username: string) => { onUsernameChange = (username: string) => {
this.setUsername(username); this.setUsername(username);
saveUsernameAndIdToLocalStorage(username, this.state.userId); saveUsernameToLocalStorage(username);
};
onUserIdChange = (userId: string) => {
this.setState({ userId });
saveUsernameAndIdToLocalStorage(this.state.username, userId);
}; };
render() { render() {

View File

@@ -37,29 +37,6 @@ class Portal {
this.roomId = id; this.roomId = id;
this.roomKey = key; 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 // Initialize socket listeners
this.socket.on("init-room", () => { this.socket.on("init-room", () => {
if (this.socket) { if (this.socket) {
@@ -77,6 +54,21 @@ class Portal {
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: string[]) => {
this.collab.setCollaborators(clients); 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() { isOpen() {
@@ -189,14 +181,13 @@ class Portal {
}; };
broadcastIdleChange = (userState: UserIdleState) => { broadcastIdleChange = (userState: UserIdleState) => {
if (this.socket) { if (this.socket?.id) {
const data: SocketUpdateDataSource["IDLE_STATUS"] = { const data: SocketUpdateDataSource["IDLE_STATUS"] = {
type: "IDLE_STATUS", type: "IDLE_STATUS",
payload: { payload: {
socketId: this.socket.id,
userState, userState,
username: this.collab.state.username, username: this.collab.state.username,
userId: this.collab.state.userId,
socketId: this.socket.id,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
@@ -210,17 +201,16 @@ class Portal {
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"]; pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
}) => { }) => {
if (this.socket) { if (this.socket?.id) {
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
type: "MOUSE_LOCATION", type: "MOUSE_LOCATION",
payload: { payload: {
socketId: this.socket.id,
pointer: payload.pointer, pointer: payload.pointer,
button: payload.button || "up", button: payload.button || "up",
selectedElementIds: selectedElementIds:
this.collab.excalidrawAPI.getAppState().selectedElementIds, this.collab.excalidrawAPI.getAppState().selectedElementIds,
username: this.collab.state.username, username: this.collab.state.username,
userId: this.collab.state.userId,
socketId: this.socket.id,
}, },
}; };
return this._broadcastSocketData( return this._broadcastSocketData(
@@ -229,23 +219,6 @@ 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; export default Portal;

View File

@@ -106,29 +106,19 @@ export type SocketUpdateDataSource = {
MOUSE_LOCATION: { MOUSE_LOCATION: {
type: "MOUSE_LOCATION"; type: "MOUSE_LOCATION";
payload: { payload: {
socketId: string;
pointer: { x: number; y: number }; pointer: { x: number; y: number };
button: "down" | "up"; button: "down" | "up";
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
username: string; username: string;
userId: string;
socketId: string;
}; };
}; };
IDLE_STATUS: { IDLE_STATUS: {
type: "IDLE_STATUS"; type: "IDLE_STATUS";
payload: { payload: {
socketId: string;
userState: UserIdleState; userState: UserIdleState;
username: string; username: string;
userId: string;
socketId: string;
};
};
USER_JOINED: {
type: "USER_JOINED";
payload: {
username: string;
userId: string;
socketId: string;
}; };
}; };
}; };

View File

@@ -8,14 +8,11 @@ import { clearElementsForLocalStorage } from "../../element";
import { STORAGE_KEYS } from "../app_constants"; import { STORAGE_KEYS } from "../app_constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
export const saveUsernameAndIdToLocalStorage = ( export const saveUsernameToLocalStorage = (username: string) => {
username: string,
userId: string,
) => {
try { try {
localStorage.setItem( localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_COLLAB, STORAGE_KEYS.LOCAL_STORAGE_COLLAB,
JSON.stringify({ username, userId }), JSON.stringify({ username }),
); );
} catch (error: any) { } catch (error: any) {
// Unable to access window.localStorage // Unable to access window.localStorage
@@ -23,14 +20,11 @@ export const saveUsernameAndIdToLocalStorage = (
} }
}; };
export const importUsernameAndIdFromLocalStorage = (): { export const importUsernameFromLocalStorage = (): string | null => {
username: string;
userId: string;
} | null => {
try { try {
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB); const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
if (data) { if (data) {
return JSON.parse(data); return JSON.parse(data).username;
} }
} catch (error: any) { } catch (error: any) {
// Unable to access localStorage // Unable to access localStorage

View File

@@ -65,7 +65,7 @@ import {
import { import {
getLibraryItemsFromStorage, getLibraryItemsFromStorage,
importFromLocalStorage, importFromLocalStorage,
importUsernameAndIdFromLocalStorage, importUsernameFromLocalStorage,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore"; import { restore, restoreAppState, RestoredDataState } from "../data/restore";
@@ -411,8 +411,7 @@ const ExcalidrawWrapper = () => {
// don't sync if local state is newer or identical to browser state // don't sync if local state is newer or identical to browser state
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage(); const localDataState = importFromLocalStorage();
const username = const username = importUsernameFromLocalStorage();
importUsernameAndIdFromLocalStorage()?.username ?? "";
let langCode = languageDetector.detect() || defaultLang.code; let langCode = languageDetector.detect() || defaultLang.code;
if (Array.isArray(langCode)) { if (Array.isArray(langCode)) {
langCode = langCode[0]; langCode = langCode[0];

View File

@@ -1,6 +1,7 @@
import { import {
getCommonBounds, getCommonBounds,
getElementAbsoluteCoords, getElementAbsoluteCoords,
getElementBounds,
isTextElement, isTextElement,
} from "./element"; } from "./element";
import { import {
@@ -16,7 +17,7 @@ import {
} from "./element/textElement"; } from "./element/textElement";
import { arrayToMap, findIndex } from "./utils"; import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement"; import { mutateElement } from "./element/mutateElement";
import { AppState } from "./types"; import { AppClassProperties, AppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element"; import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex"; import { moveOneRight } from "./zindex";
@@ -299,6 +300,15 @@ export const groupsAreCompletelyOutOfFrame = (
); );
}; };
export const isPointInFrame = (
{ x, y }: { x: number; y: number },
frame: ExcalidrawFrameElement,
) => {
const [x1, y1, x2, y2] = getElementBounds(frame);
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
};
// --------------------------- Frame Utils ------------------------------------ // --------------------------- Frame Utils ------------------------------------
/** /**
@@ -571,8 +581,13 @@ export const replaceAllElementsInFrame = (
export const updateFrameMembershipOfSelectedElements = ( export const updateFrameMembershipOfSelectedElements = (
allElements: ExcalidrawElementsIncludingDeleted, allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState, appState: AppState,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements(allElements, appState); const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements: allElements,
});
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements); const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
if (appState.editingGroupId) { if (appState.editingGroupId) {

View File

@@ -1,5 +1,10 @@
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types"; import {
import { AppState } from "./types"; GroupId,
ExcalidrawElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppClassProperties, AppState } from "./types";
import { getSelectedElements } from "./scene"; import { getSelectedElements } from "./scene";
import { getBoundTextElement } from "./element/textElement"; import { getBoundTextElement } from "./element/textElement";
import { makeNextSelectedElementIds } from "./scene/selection"; import { makeNextSelectedElementIds } from "./scene/selection";
@@ -67,12 +72,23 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
*/ */
export const selectGroupsForSelectedElements = ( export const selectGroupsForSelectedElements = (
appState: AppState, appState: AppState,
elements: readonly NonDeleted<ExcalidrawElement>[], elements: readonly NonDeletedExcalidrawElement[],
prevAppState: AppState, prevAppState: AppState,
/**
* supply null in cases where you don't have access to App instance and
* you don't care about optimizing selectElements retrieval
*/
app: AppClassProperties | null,
): AppState => { ): AppState => {
let nextAppState: AppState = { ...appState, selectedGroupIds: {} }; let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app
? app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements,
})
: getSelectedElements(elements, appState);
if (!selectedElements.length) { if (!selectedElements.length) {
return { return {

View File

@@ -411,8 +411,7 @@
"fileSavedToFilename": "Saved to {filename}", "fileSavedToFilename": "Saved to {filename}",
"canvas": "canvas", "canvas": "canvas",
"selection": "selection", "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": { "colors": {
"transparent": "Transparent", "transparent": "Transparent",

View File

@@ -619,8 +619,8 @@ export const _renderScene = ({
if (renderConfig.remoteSelectedElementIds[element.id]) { if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push( selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map( ...renderConfig.remoteSelectedElementIds[element.id].map(
(userId) => { (socketId) => {
const background = getClientColor(userId); const background = getClientColor(socketId);
return background; return background;
}, },
), ),

View File

@@ -11,6 +11,9 @@ import {
} from "../element"; } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { isFrameElement } from "../element/typeChecks"; import { isFrameElement } from "../element/typeChecks";
import { getSelectedElements } from "./selection";
import { AppState } from "../types";
import { Assert, SameType } from "../utility-types";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"]; type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey; type ElementKey = ExcalidrawElement | ElementIdKey;
@@ -18,6 +21,31 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void; type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void; type SceneStateCallbackRemover = () => void;
type SelectionHash = string & { __brand: "selectionHash" };
const hashSelectionOpts = (
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
) => {
const keys = ["includeBoundTextElement", "includeElementsInFrames"] as const;
type HashableKeys = Omit<typeof opts, "selectedElementIds" | "elements">;
// just to ensure we're hashing all expected keys
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type _ = Assert<
SameType<
Required<HashableKeys>,
Pick<Required<HashableKeys>, typeof keys[number]>
>
>;
let hash = "";
for (const key of keys) {
hash += `${key}:${opts[key] ? "1" : "0"}`;
}
return hash as SelectionHash;
};
// ideally this would be a branded type but it'd be insanely hard to work with // ideally this would be a branded type but it'd be insanely hard to work with
// in our codebase // in our codebase
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[]; export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
@@ -68,6 +96,15 @@ class Scene {
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = []; private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
private frames: readonly ExcalidrawFrameElement[] = []; private frames: readonly ExcalidrawFrameElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>(); private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
private selectedElementsCache: {
selectedElementIds: AppState["selectedElementIds"] | null;
elements: readonly NonDeletedExcalidrawElement[] | null;
cache: Map<SelectionHash, NonDeletedExcalidrawElement[]>;
} = {
selectedElementIds: null,
elements: null,
cache: new Map(),
};
getElementsIncludingDeleted() { getElementsIncludingDeleted() {
return this.elements; return this.elements;
@@ -81,6 +118,52 @@ class Scene {
return this.frames; return this.frames;
} }
getSelectedElements(opts: {
// NOTE can be ommitted by making Scene constructor require App instance
selectedElementIds: AppState["selectedElementIds"];
/**
* for specific cases where you need to use elements not from current
* scene state. This in effect will likely result in cache-miss, and
* the cache won't be updated in this case.
*/
elements?: readonly ExcalidrawElement[];
// selection-related options
includeBoundTextElement?: boolean;
includeElementsInFrames?: boolean;
}): NonDeleted<ExcalidrawElement>[] {
const hash = hashSelectionOpts(opts);
const elements = opts?.elements || this.nonDeletedElements;
if (
this.selectedElementsCache.elements === elements &&
this.selectedElementsCache.selectedElementIds === opts.selectedElementIds
) {
const cached = this.selectedElementsCache.cache.get(hash);
if (cached) {
return cached;
}
} else if (opts?.elements == null) {
// if we're operating on latest scene elements and the cache is not
// storing the latest elements, clear the cache
this.selectedElementsCache.cache.clear();
}
const selectedElements = getSelectedElements(
elements,
{ selectedElementIds: opts.selectedElementIds },
opts,
);
// cache only if we're not using custom elements
if (opts?.elements == null) {
this.selectedElementsCache.selectedElementIds = opts.selectedElementIds;
this.selectedElementsCache.elements = this.nonDeletedElements;
this.selectedElementsCache.cache.set(hash, selectedElements);
}
return selectedElements;
}
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] { getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
return this.nonDeletedFrames; return this.nonDeletedFrames;
} }
@@ -168,11 +251,21 @@ class Scene {
} }
destroy() { destroy() {
this.nonDeletedElements = [];
this.elements = [];
this.nonDeletedFrames = [];
this.frames = [];
this.elementsMap.clear();
this.selectedElementsCache.selectedElementIds = null;
this.selectedElementsCache.elements = null;
this.selectedElementsCache.cache.clear();
Scene.sceneMapById.forEach((scene, elementKey) => { Scene.sceneMapById.forEach((scene, elementKey) => {
if (scene === this) { if (scene === this) {
Scene.sceneMapById.delete(elementKey); Scene.sceneMapById.delete(elementKey);
} }
}); });
// done not for memory leaks, but to guard against possible late fires // done not for memory leaks, but to guard against possible late fires
// (I guess?) // (I guess?)
this.callbacks.clear(); this.callbacks.clear();

View File

@@ -1527,14 +1527,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -1586,14 +1586,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4271,14 +4271,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4303,14 +4303,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 400692809, "versionNonce": 238820263,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@@ -4362,14 +4362,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4405,14 +4405,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4434,14 +4434,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 2019559783, "versionNonce": 401146281,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@@ -4482,14 +4482,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1116226695, "versionNonce": 1150084233,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4513,14 +4513,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@@ -4557,14 +4557,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -4586,14 +4586,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 400692809, "versionNonce": 238820263,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@@ -5585,14 +5585,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -5619,14 +5619,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,
@@ -5678,14 +5678,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -5721,14 +5721,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -5750,14 +5750,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 2019559783, "versionNonce": 401146281,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,
@@ -5798,14 +5798,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@@ -5829,14 +5829,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,7 @@ const populateElements = (
{ ...h.state, ...appState, selectedElementIds }, { ...h.state, ...appState, selectedElementIds },
h.elements, h.elements,
h.state, h.state,
null,
), ),
...appState, ...appState,
selectedElementIds, selectedElementIds,

View File

@@ -55,7 +55,6 @@ export type Collaborator = {
avatarUrl?: string; avatarUrl?: string;
// user id. If supplied, we'll filter out duplicates when rendering user avatars. // user id. If supplied, we'll filter out duplicates when rendering user avatars.
id?: string; id?: string;
socketId?: string;
}; };
export type DataURL = string & { _brand: "DataURL" }; export type DataURL = string & { _brand: "DataURL" };
@@ -377,12 +376,6 @@ export enum UserIdleState {
IDLE = "idle", IDLE = "idle",
} }
export enum PauseCollaborationState {
PAUSED = "paused",
RESUMED = "resumed",
SYNCED = "synced",
}
export type ExportOpts = { export type ExportOpts = {
saveFileToDisk?: boolean; saveFileToDisk?: boolean;
onExportToBackend?: ( onExportToBackend?: (

View File

@@ -47,3 +47,6 @@ export type ForwardRef<T, P = any> = Parameters<
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U> export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
? U ? U
: never; : never;
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
export type Assert<T extends true> = T;

View File

@@ -907,14 +907,3 @@ 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;
};

193
yarn.lock
View File

@@ -2329,11 +2329,6 @@
dependencies: dependencies:
"@sinonjs/commons" "^1.7.0" "@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": "@surma/rollup-plugin-off-main-thread@^2.2.3":
version "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" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -3173,6 +3168,11 @@ adjust-sourcemap-loader@^4.0.0:
loader-utils "^2.0.0" loader-utils "^2.0.0"
regex-parser "^2.2.11" 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: agent-base@6:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -3398,6 +3398,11 @@ array.prototype.tosorted@^1.1.1:
es-shim-unscopables "^1.0.0" es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3" 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: asap@~2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@@ -3418,6 +3423,11 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== 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: async@^2.6.4:
version "2.6.4" version "2.6.4"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
@@ -3610,6 +3620,11 @@ babel-preset-react-app@^10.0.1:
babel-plugin-macros "^3.1.0" babel-plugin-macros "^3.1.0"
babel-plugin-transform-react-remove-prop-types "^0.4.24" 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: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -3620,6 +3635,11 @@ base64-arraybuffer-es6@^0.7.0:
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86" resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86"
integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw== 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: basic-auth@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
@@ -3652,6 +3672,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 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: bluebird@^3.5.5:
version "3.7.2" version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@@ -4052,6 +4077,21 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== 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: compressible@~2.0.16:
version "2.0.18" version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -4424,7 +4464,7 @@ debug@2.6.9, debug@^2.6.0:
dependencies: dependencies:
ms "2.0.0" 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.3.1, debug@~4.3.2: 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:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -4438,6 +4478,13 @@ debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" 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: decimal.js@^10.2.1:
version "10.4.3" version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@@ -4771,21 +4818,33 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
engine.io-client@~6.4.0: engine.io-client@~3.4.0:
version "6.4.0" version "3.4.4"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967"
integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.1.0" component-emitter "~1.3.0"
debug "~4.3.1" component-inherit "0.0.3"
engine.io-parser "~5.0.3" debug "~3.1.0"
ws "~8.11.0" engine.io-parser "~2.2.0"
xmlhttprequest-ssl "~2.0.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"
engine.io-parser@~5.0.3: engine.io-parser@~2.2.0:
version "5.0.7" version "2.2.1"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.7.tgz#ed5eae76c71f398284c578ab6deafd3ba7e4e4f6" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
integrity sha512-P+jDFbvK6lE3n1OL+q9KuzdOFWkkZ/cMV9gol/SbVfpyqfvrfrFTOFJ6fQm2VC3PZHlU3QPhVwmbsCnauHF2MQ== 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"
enhanced-resolve@^5.10.0: enhanced-resolve@^5.10.0:
version "5.12.0" version "5.12.0"
@@ -5856,6 +5915,18 @@ has-bigints@^1.0.1, has-bigints@^1.0.2:
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== 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: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -6180,6 +6251,11 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== 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: inflight@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -6463,6 +6539,11 @@ is-wsl@^2.2.0:
dependencies: dependencies:
is-docker "^2.0.0" 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: isarray@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@@ -8003,6 +8084,16 @@ parse5@6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== 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: parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -9676,23 +9767,31 @@ sliced@^1.0.1:
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA== integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==
socket.io-client@4.6.1: socket.io-client@2.3.1:
version "4.6.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.1.tgz#91a4038ef4d03c19967bb3c646fec6e0eaa78cff"
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== integrity sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.1.0" backo2 "1.0.2"
debug "~4.3.2" component-bind "1.0.0"
engine.io-client "~6.4.0" component-emitter "~1.3.0"
socket.io-parser "~4.2.1" 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-parser@~4.2.1: socket.io-parser@~3.3.0:
version "4.2.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.1.0" component-emitter "~1.3.0"
debug "~4.3.1" debug "~3.1.0"
isarray "2.0.1"
sockjs@^0.3.24: sockjs@^0.3.24:
version "0.3.24" version "0.3.24"
@@ -10226,6 +10325,11 @@ tmpl@1.0.5:
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== 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: to-fast-properties@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -11049,10 +11153,12 @@ ws@^8.13.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
ws@~8.11.0: ws@~6.1.0:
version "8.11.0" version "6.1.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
dependencies:
async-limiter "~1.0.0"
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"
@@ -11064,10 +11170,10 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest-ssl@~2.0.0: xmlhttprequest-ssl@~1.5.4:
version "2.0.0" version "1.5.5"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== integrity sha512-/bFPLUgJrfGUL10AIv4Y7/CUt6so9CLtB/oFxQSHseSDNNCdC6vwwKEqwLN6wNPBg9YWXAiMu8jkf6RPRS/75Q==
xmlhttprequest@1.8.0: xmlhttprequest@1.8.0:
version "1.8.0" version "1.8.0"
@@ -11112,6 +11218,11 @@ yargs@^16.2.0:
y18n "^5.0.5" y18n "^5.0.5"
yargs-parser "^20.2.2" 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: yocto-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"