Compare commits

..

1 Commits

Author SHA1 Message Date
dwelle
b8da178ebd feat: update eye-dropper icon 2023-07-07 12:22:52 +02:00
34 changed files with 614 additions and 1370 deletions

View File

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

View File

@@ -31,7 +31,6 @@ import {
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { getFontString } from "../utils";
import { register } from "./register";
@@ -212,7 +211,7 @@ export const actionWrapTextInContainer = register({
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
const containerIds: AppState["selectedElementIds"] = {};
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {

View File

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

View File

@@ -125,6 +125,13 @@ export const actionFinalize = register({
{ x, y },
);
}
if (
!appState.activeTool.locked &&
appState.activeTool.type !== "freedraw"
) {
appState.selectedElementIds[multiPointElement.id] = true;
}
}
if (

View File

@@ -90,8 +90,8 @@ export const actionRemoveAllElementsFromFrame = register({
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionupdateFrameRendering = register({
name: "updateFrameRendering",
export const actionToggleFrameRendering = register({
name: "toggleFrameRendering",
viewMode: true,
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
@@ -99,16 +99,13 @@ export const actionupdateFrameRendering = register({
elements,
appState: {
...appState,
frameRendering: {
...appState.frameRendering,
enabled: !appState.frameRendering.enabled,
},
shouldRenderFrames: !appState.shouldRenderFrames,
},
commitToHistory: false,
};
},
contextItemLabel: "labels.updateFrameRendering",
checked: (appState: AppState) => appState.frameRendering.enabled,
contextItemLabel: "labels.toggleFrameRendering",
checked: (appState: AppState) => appState.shouldRenderFrames,
});
export const actionSetFrameAsActiveTool = register({

View File

@@ -218,7 +218,6 @@ export const actionUngroup = register({
const updateAppState = selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
appState,
);
frames.forEach((frame) => {
@@ -233,18 +232,9 @@ export const actionUngroup = register({
});
// remove binded text elements from selection
updateAppState.selectedElementIds = Object.entries(
updateAppState.selectedElementIds,
).reduce(
(acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
if (selected && !boundTextElementIds.includes(id)) {
acc[id] = true;
}
return acc;
},
{},
boundTextElementIds.forEach(
(id) => (updateAppState.selectedElementIds[id] = false),
);
return {
appState: updateAppState,
elements: nextElements,

View File

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

View File

@@ -119,7 +119,7 @@ export type ActionName =
| "toggleHandTool"
| "selectAllElementsInFrame"
| "removeAllElementsFromFrame"
| "updateFrameRendering"
| "toggleFrameRendering"
| "setFrameAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";

View File

@@ -84,7 +84,7 @@ export const getDefaultAppState = (): Omit<
showStats: false,
startBoundElement: null,
suggestedBindings: [],
frameRendering: { enabled: true, clip: true, name: true, outline: true },
shouldRenderFrames: true,
frameToHighlight: null,
editingFrame: null,
elementsToHighlight: null,
@@ -191,7 +191,7 @@ const APP_STATE_STORAGE_CONF = (<
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
frameRendering: { browser: false, export: false, server: false },
shouldRenderFrames: { browser: false, export: false, server: false },
frameToHighlight: { browser: false, export: false, server: false },
editingFrame: { browser: false, export: false, server: false },
elementsToHighlight: { browser: false, export: false, server: false },

View File

@@ -315,10 +315,7 @@ import {
updateFrameMembershipOfSelectedElements,
isElementInFrame,
} from "../frame";
import {
excludeElementsInFramesFromSelection,
makeNextSelectedElementIds,
} from "../scene/selection";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
import { actionPaste } from "../actions/actionClipboard";
import {
actionRemoveAllElementsFromFrame,
@@ -505,7 +502,7 @@ class App extends React.Component<AppProps, AppState> {
setActiveTool: this.setActiveTool,
setCursor: this.setCursor,
resetCursor: this.resetCursor,
updateFrameRendering: this.updateFrameRendering,
toggleFrameRendering: this.toggleFrameRendering,
toggleSidebar: this.toggleSidebar,
} as const;
if (typeof excalidrawRef === "function") {
@@ -651,7 +648,7 @@ class App extends React.Component<AppProps, AppState> {
};
private renderFrameNames = () => {
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
if (!this.state.shouldRenderFrames) {
return null;
}
@@ -1356,7 +1353,6 @@ class App extends React.Component<AppProps, AppState> {
this.scene.destroy();
this.library.destroy();
clearTimeout(touchTimeout);
isSomeElementSelected.clearCache();
touchTimeout = 0;
}
@@ -1829,7 +1825,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.touches.length === 2) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
});
}
};
@@ -1839,10 +1835,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.touches.length > 0) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state,
),
selectedElementIds: this.state.previousSelectedElementIds,
});
} else {
gesture.pointers.clear();
@@ -1902,14 +1895,7 @@ class App extends React.Component<AppProps, AppState> {
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[imageElement.id]: true,
},
this.state,
),
});
this.setState({ selectedElementIds: { [imageElement.id]: true } });
return;
}
@@ -2046,7 +2032,6 @@ class App extends React.Component<AppProps, AppState> {
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
this.state,
),
() => {
if (opts.files) {
@@ -2145,9 +2130,8 @@ class App extends React.Component<AppProps, AppState> {
}
this.setState({
selectedElementIds: makeNextSelectedElementIds(
Object.fromEntries(textElements.map((el) => [el.id, true])),
this.state,
selectedElementIds: Object.fromEntries(
textElements.map((el) => [el.id, true]),
),
});
@@ -2208,23 +2192,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
updateFrameRendering = (
opts:
| Partial<AppState["frameRendering"]>
| ((
prevState: AppState["frameRendering"],
) => Partial<AppState["frameRendering"]>),
) => {
toggleFrameRendering = () => {
this.setState((prevState) => {
const next =
typeof opts === "function" ? opts(prevState.frameRendering) : opts;
return {
frameRendering: {
enabled: next?.enabled ?? prevState.frameRendering.enabled,
clip: next?.clip ?? prevState.frameRendering.clip,
name: next?.name ?? prevState.frameRendering.name,
outline: next?.outline ?? prevState.frameRendering.outline,
},
shouldRenderFrames: !prevState.shouldRenderFrames,
};
});
};
@@ -2377,7 +2348,6 @@ class App extends React.Component<AppProps, AppState> {
toast: {
message: string;
closable?: boolean;
spinner?: boolean;
duration?: number;
} | null,
) => {
@@ -2779,7 +2749,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
setCursorForShape(this.canvas, this.state);
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -2824,7 +2794,7 @@ class App extends React.Component<AppProps, AppState> {
if (nextActiveTool.type !== "selection") {
this.setState({
activeTool: nextActiveTool,
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -2861,7 +2831,7 @@ class App extends React.Component<AppProps, AppState> {
// elements by mistake while zooming
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
});
}
gesture.initialScale = this.state.zoom.value;
@@ -2906,10 +2876,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state,
),
selectedElementIds: this.state.previousSelectedElementIds,
});
}
gesture.initialScale = null;
@@ -2974,13 +2941,10 @@ class App extends React.Component<AppProps, AppState> {
? element.containerId
: element.id;
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[elementIdToSelect]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[elementIdToSelect]: true,
},
}));
}
if (isDeleted) {
@@ -3016,7 +2980,7 @@ class App extends React.Component<AppProps, AppState> {
private deselectElements() {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -3103,9 +3067,7 @@ class App extends React.Component<AppProps, AppState> {
).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element);
return containingFrame &&
this.state.frameRendering.enabled &&
this.state.frameRendering.clip
return containingFrame && this.state.shouldRenderFrames
? isCursorInFrame({ x, y }, containingFrame)
: true;
});
@@ -3329,7 +3291,6 @@ class App extends React.Component<AppProps, AppState> {
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
prevState,
),
);
return;
@@ -4037,15 +3998,12 @@ class App extends React.Component<AppProps, AppState> {
editingElement: null,
startBoundElement: null,
suggestedBindings: [],
selectedElementIds: makeNextSelectedElementIds(
Object.keys(this.state.selectedElementIds)
.filter((key) => key !== element.id)
.reduce((obj: { [id: string]: true }, key) => {
obj[key] = this.state.selectedElementIds[key];
return obj;
}, {}),
this.state,
),
selectedElementIds: Object.keys(this.state.selectedElementIds)
.filter((key) => key !== element.id)
.reduce((obj: { [id: string]: boolean }, key) => {
obj[key] = this.state.selectedElementIds[key];
return obj;
}, {}),
},
});
return;
@@ -4514,7 +4472,7 @@ class App extends React.Component<AppProps, AppState> {
private clearSelectionIfNotUsingSelection = (): void => {
if (this.state.activeTool.type !== "selection") {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -4646,12 +4604,9 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.editingLinearElement) {
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[this.state.editingLinearElement.elementId]: true,
},
this.state,
),
selectedElementIds: {
[this.state.editingLinearElement.elementId]: true,
},
});
// If we click on something
} else if (hitElement != null) {
@@ -4679,7 +4634,7 @@ class App extends React.Component<AppProps, AppState> {
!isElementInGroup(hitElement, this.state.editingGroupId)
) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -4695,7 +4650,7 @@ class App extends React.Component<AppProps, AppState> {
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
) {
this.setState((prevState) => {
const nextSelectedElementIds: { [id: string]: true } = {
const nextSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement.id]: true,
};
@@ -4713,13 +4668,13 @@ class App extends React.Component<AppProps, AppState> {
previouslySelectedElements,
hitElement.id,
).forEach((element) => {
delete nextSelectedElementIds[element.id];
nextSelectedElementIds[element.id] = false;
});
} else if (hitElement.frameId) {
// if hitElement is in a frame and its frame has been selected
// disable selection for the given element
if (nextSelectedElementIds[hitElement.frameId]) {
delete nextSelectedElementIds[hitElement.id];
nextSelectedElementIds[hitElement.id] = false;
}
} else {
// hitElement is neither a frame nor an element in a frame
@@ -4749,7 +4704,7 @@ class App extends React.Component<AppProps, AppState> {
framesInGroups.has(element.frameId)
) {
// deselect element and groups containing the element
delete nextSelectedElementIds[element.id];
nextSelectedElementIds[element.id] = false;
element.groupIds
.flatMap((gid) =>
getElementsInGroup(
@@ -4757,9 +4712,10 @@ class App extends React.Component<AppProps, AppState> {
gid,
),
)
.forEach((element) => {
delete nextSelectedElementIds[element.id];
});
.forEach(
(element) =>
(nextSelectedElementIds[element.id] = false),
);
}
});
}
@@ -4772,7 +4728,6 @@ class App extends React.Component<AppProps, AppState> {
showHyperlinkPopup: hitElement.link ? "info" : false,
},
this.scene.getNonDeletedElements(),
prevState,
);
});
pointerDownState.hit.wasAddedToSelection = true;
@@ -4889,18 +4844,12 @@ class App extends React.Component<AppProps, AppState> {
frameId: topLayerFrame ? topLayerFrame.id : null,
});
this.setState((prevState) => {
const nextSelectedElementIds = {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState,
),
};
});
[element.id]: false,
},
}));
const pressures = element.simulatePressure
? element.pressures
@@ -4996,13 +4945,10 @@ class App extends React.Component<AppProps, AppState> {
}
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[multiElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[multiElement.id]: true,
},
}));
// clicking outside commit zone → update reference for last committed
// point
@@ -5053,18 +4999,12 @@ class App extends React.Component<AppProps, AppState> {
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
this.setState((prevState) => {
const nextSelectedElementIds = {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState,
),
};
});
[element.id]: false,
},
}));
mutateElement(element, {
points: [...element.points, [0, 0]],
});
@@ -5438,16 +5378,15 @@ class App extends React.Component<AppProps, AppState> {
const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element;
const elements = this.scene.getElementsIncludingDeleted();
const selectedElementIds = new Set(
const selectedElementIds: Array<ExcalidrawElement["id"]> =
getSelectedElements(elements, this.state, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}).map((element) => element.id),
);
}).map((element) => element.id);
for (const element of elements) {
if (
selectedElementIds.has(element.id) ||
selectedElementIds.includes(element.id) ||
// case: the state.selectedElementIds might not have been
// updated yet by the time this mousemove event is fired
(element.id === hitElement?.id &&
@@ -5585,9 +5524,14 @@ class App extends React.Component<AppProps, AppState> {
},
},
this.scene.getNonDeletedElements(),
prevState,
),
);
} else {
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
}
// box-select line editor points
@@ -5603,29 +5547,28 @@ class App extends React.Component<AppProps, AppState> {
elements,
draggingElement,
);
this.setState((prevState) => {
const nextSelectedElementIds = elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
acc[element.id] = true;
return acc;
},
{},
);
if (pointerDownState.hit.element) {
// if using ctrl/cmd, select the hitElement only if we
// haven't box-selected anything else
if (!elementsWithinSelection.length) {
nextSelectedElementIds[pointerDownState.hit.element.id] = true;
} else {
delete nextSelectedElementIds[pointerDownState.hit.element.id];
}
}
return selectGroupsForSelectedElements(
this.setState((prevState) =>
selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: nextSelectedElementIds,
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
acc[element.id] = true;
return acc;
},
{},
),
...(pointerDownState.hit.element
? {
// if using ctrl/cmd, select the hitElement only if we
// haven't box-selected anything else
[pointerDownState.hit.element.id]:
!elementsWithinSelection.length,
}
: null),
},
showHyperlinkPopup:
elementsWithinSelection.length === 1 &&
elementsWithinSelection[0].link
@@ -5642,9 +5585,8 @@ class App extends React.Component<AppProps, AppState> {
: null,
},
this.scene.getNonDeletedElements(),
prevState,
);
});
),
);
}
}
});
@@ -5838,12 +5780,7 @@ class App extends React.Component<AppProps, AppState> {
try {
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
},
{ selectedElementIds: { [imageElement.id]: true } },
() => {
this.actionManager.executeAction(actionFinalize);
},
@@ -5907,13 +5844,10 @@ class App extends React.Component<AppProps, AppState> {
activeTool: updateActiveTool(this.state, {
type: "selection",
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
selectedLinearElement: new LinearElementEditor(
draggingElement,
this.scene,
@@ -6207,37 +6141,31 @@ class App extends React.Component<AppProps, AppState> {
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) {
this.setState((_prevState) => {
const nextSelectedElementIds = {
..._prevState.selectedElementIds,
};
// We want to unselect all groups hitElement is part of
// as well as all elements that are part of the groups
// hitElement is part of
for (const groupedElement of hitElement.groupIds.flatMap(
(groupId) =>
getElementsInGroup(
this.scene.getNonDeletedElements(),
groupId,
),
)) {
delete nextSelectedElementIds[groupedElement.id];
}
return {
selectedGroupIds: {
..._prevState.selectedElementIds,
...hitElement.groupIds
.map((gId) => ({ [gId]: false }))
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
},
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
_prevState,
// We want to unselect all groups hitElement is part of
// as well as all elements that are part of the groups
// hitElement is part of
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
.flatMap((groupId) =>
getElementsInGroup(
this.scene.getNonDeletedElements(),
groupId,
),
};
});
)
.map((element) => ({ [element.id]: false }))
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
this.setState((_prevState) => ({
selectedGroupIds: {
..._prevState.selectedElementIds,
...hitElement.groupIds
.map((gId) => ({ [gId]: false }))
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
},
selectedElementIds: {
..._prevState.selectedElementIds,
...idsOfSelectedElementsThatAreInGroups,
},
}));
// if not gragging a linear element point (outside editor)
} else if (!this.state.selectedLinearElement?.isDragging) {
// remove element from selection while
@@ -6246,8 +6174,8 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => {
const newSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement!.id]: false,
};
delete newSelectedElementIds[hitElement!.id];
const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
{ ...prevState, selectedElementIds: newSelectedElementIds },
@@ -6268,7 +6196,6 @@ class App extends React.Component<AppProps, AppState> {
: prevState.selectedLinearElement,
},
this.scene.getNonDeletedElements(),
prevState,
);
});
}
@@ -6279,23 +6206,21 @@ class App extends React.Component<AppProps, AppState> {
// when hitElement is part of a selected frame, deselect the frame
// to avoid frame and containing elements selected simultaneously
this.setState((prevState) => {
const nextSelectedElementIds: {
[id: string]: true;
} = {
const nextSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement.id]: true,
// deselect the frame
[hitElement.frameId!]: false,
};
// deselect the frame
delete nextSelectedElementIds[hitElement.frameId!];
// deselect groups containing the frame
(this.scene.getElement(hitElement.frameId!)?.groupIds ?? [])
.flatMap((gid) =>
getElementsInGroup(this.scene.getNonDeletedElements(), gid),
)
.forEach((element) => {
delete nextSelectedElementIds[element.id];
});
.forEach(
(element) => (nextSelectedElementIds[element.id] = false),
);
return selectGroupsForSelectedElements(
{
@@ -6304,19 +6229,15 @@ class App extends React.Component<AppProps, AppState> {
showHyperlinkPopup: hitElement.link ? "info" : false,
},
this.scene.getNonDeletedElements(),
prevState,
);
});
} else {
// add element to selection while keeping prev elements selected
this.setState((_prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
..._prevState.selectedElementIds,
[hitElement!.id]: true,
},
_prevState,
),
selectedElementIds: {
..._prevState.selectedElementIds,
[hitElement!.id]: true,
},
}));
}
} else {
@@ -6334,7 +6255,6 @@ class App extends React.Component<AppProps, AppState> {
: prevState.selectedLinearElement,
},
this.scene.getNonDeletedElements(),
prevState,
),
}));
}
@@ -6359,7 +6279,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
// Deselect selected elements
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -6370,17 +6290,13 @@ class App extends React.Component<AppProps, AppState> {
if (
!activeTool.locked &&
activeTool.type !== "freedraw" &&
draggingElement &&
draggingElement.type !== "selection"
draggingElement
) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
}));
}
@@ -6694,10 +6610,7 @@ class App extends React.Component<AppProps, AppState> {
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
selectedElementIds: { [imageElement.id]: true },
},
() => {
this.actionManager.executeAction(actionFinalize);
@@ -6924,7 +6837,7 @@ class App extends React.Component<AppProps, AppState> {
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
selectedElementIds: {},
selectedGroupIds: {},
// Continue editing the same group if the user selected a different
// element from it
@@ -6936,7 +6849,7 @@ class App extends React.Component<AppProps, AppState> {
: null,
}));
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
previousSelectedElementIds: this.state.selectedElementIds,
});
}
@@ -7005,12 +6918,7 @@ class App extends React.Component<AppProps, AppState> {
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
});
this.setState({ selectedElementIds: { [imageElement.id]: true } });
return;
}
@@ -7135,7 +7043,6 @@ class App extends React.Component<AppProps, AppState> {
: null,
},
this.scene.getNonDeletedElements(),
this.state,
)
: this.state),
showHyperlinkPopup: false,

View File

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

View File

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

View File

@@ -1620,11 +1620,18 @@ export const alertTriangleIcon = createIcon(
export const eyeDropperIcon = createIcon(
<g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M11 7l6 6"></path>
<path d="M4 16l11.7 -11.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-11.7 11.7h-4v-4z"></path>
<path d="m9.168 5.833 5 5M9.999 6.667l-6.667 6.666v3.334h3.333L13.332 10M9.999 6.667l3.083-3.084a.833.833 0 0 1 1.167 0l2.166 2.167a.833.833 0 0 1 0 1.167L13.332 10M9.999 6.667 13.332 10" />
<rect
x="13.684"
y="2.976"
width="4.751"
height="5.106"
rx="1"
transform="rotate(45 13.684 2.976)"
fill="currentColor"
/>
</g>,
tablerIconProps,
modifiedTablerIconProps,
);
export const extraToolsIcon = createIcon(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,135 +0,0 @@
declare global {
interface Window {
debug: typeof Debug;
}
}
const lessPrecise = (num: number, precision = 5) =>
parseFloat(num.toPrecision(precision));
const getAvgFrameTime = (times: number[]) =>
lessPrecise(times.reduce((a, b) => a + b) / times.length);
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
export class Debug {
public static DEBUG_LOG_TIMES = true;
private static TIMES_AGGR: Record<string, { t: number; times: number[] }> =
{};
private static TIMES_AVG: Record<
string,
{ t: number; times: number[]; avg: number | null }
> = {};
private static LAST_DEBUG_LOG_CALL = 0;
private static DEBUG_LOG_INTERVAL_ID: null | number = null;
private static setupInterval = () => {
if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
console.info("%c(starting perf recording)", "color: lime");
Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
}
Debug.LAST_DEBUG_LOG_CALL = Date.now();
};
private static debugLogger = () => {
if (
Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
Debug.DEBUG_LOG_INTERVAL_ID !== null
) {
window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
Debug.DEBUG_LOG_INTERVAL_ID = null;
for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) {
if (avg != null) {
console.info(
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
"color: blue",
);
}
}
console.info("%c(stopping perf recording)", "color: red");
Debug.TIMES_AGGR = {};
Debug.TIMES_AVG = {};
return;
}
if (Debug.DEBUG_LOG_TIMES) {
for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
if (times.length) {
console.info(
name,
lessPrecise(times.reduce((a, b) => a + b)),
times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
);
Debug.TIMES_AGGR[name] = { t, times: [] };
}
}
for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
if (times.length) {
const avgFrameTime = getAvgFrameTime(times);
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
Debug.TIMES_AVG[name] = {
t,
times: [],
avg:
avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
};
}
}
}
};
public static logTime = (time?: number, name = "default") => {
Debug.setupInterval();
const now = performance.now();
const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || {
t: 0,
times: [],
});
if (t) {
times.push(time != null ? time : now - t);
}
Debug.TIMES_AGGR[name].t = now;
};
public static logTimeAverage = (time?: number, name = "default") => {
Debug.setupInterval();
const now = performance.now();
const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || {
t: 0,
times: [],
});
if (t) {
times.push(time != null ? time : now - t);
}
Debug.TIMES_AVG[name].t = now;
};
private static logWrapper =
(type: "logTime" | "logTimeAverage") =>
<T extends any[], R>(fn: (...args: T) => R, name = "default") => {
return (...args: T) => {
const t0 = performance.now();
const ret = fn(...args);
Debug.logTime(performance.now() - t0, name);
return ret;
};
};
public static logTimeWrap = Debug.logWrapper("logTime");
public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage");
public static perfWrap = <T extends any[], R>(
fn: (...args: T) => R,
name = "default",
) => {
return (...args: T) => {
// eslint-disable-next-line no-console
console.time(name);
const ret = fn(...args);
// eslint-disable-next-line no-console
console.timeEnd(name);
return ret;
};
};
}
window.debug = Debug;

View File

@@ -65,7 +65,7 @@ import {
import {
getLibraryItemsFromStorage,
importFromLocalStorage,
importUsernameAndIdFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
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
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage();
const username =
importUsernameAndIdFromLocalStorage()?.username ?? "";
const username = importUsernameFromLocalStorage();
let langCode = languageDetector.detect() || defaultLang.code;
if (Array.isArray(langCode)) {
langCode = langCode[0];

View File

@@ -2,7 +2,6 @@ import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
import { AppState } from "./types";
import { getSelectedElements } from "./scene";
import { getBoundTextElement } from "./element/textElement";
import { makeNextSelectedElementIds } from "./scene/selection";
export const selectGroup = (
groupId: GroupId,
@@ -68,21 +67,13 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
export const selectGroupsForSelectedElements = (
appState: AppState,
elements: readonly NonDeleted<ExcalidrawElement>[],
prevAppState: AppState,
): AppState => {
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
const selectedElements = getSelectedElements(elements, appState);
if (!selectedElements.length) {
return {
...nextAppState,
editingGroupId: null,
selectedElementIds: makeNextSelectedElementIds(
nextAppState.selectedElementIds,
prevAppState,
),
};
return { ...nextAppState, editingGroupId: null };
}
for (const selectedElement of selectedElements) {
@@ -100,11 +91,6 @@ export const selectGroupsForSelectedElements = (
}
}
nextAppState.selectedElementIds = makeNextSelectedElementIds(
nextAppState.selectedElementIds,
prevAppState,
);
return nextAppState;
};

View File

@@ -411,8 +411,7 @@
"fileSavedToFilename": "Saved to {filename}",
"canvas": "canvas",
"selection": "selection",
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
"reconnectRoomServer": "Reconnecting to server"
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor"
},
"colors": {
"transparent": "Transparent",

View File

@@ -931,11 +931,7 @@ export const renderElement = (
break;
}
case "frame": {
if (
!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.outline
) {
if (!renderConfig.isExporting && appState.shouldRenderFrames) {
context.save();
context.translate(
element.x + renderConfig.scrollX,

View File

@@ -470,9 +470,7 @@ export const _renderScene = ({
if (
frameId &&
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
(!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.clip))
(!renderConfig.isExporting && appState.shouldRenderFrames))
) {
context.save();
@@ -619,8 +617,8 @@ export const _renderScene = ({
if (renderConfig.remoteSelectedElementIds[element.id]) {
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(userId) => {
const background = getClientColor(userId);
(socketId) => {
const background = getClientColor(socketId);
return background;
},
),

View File

@@ -1,35 +0,0 @@
import { makeNextSelectedElementIds } from "./selection";
describe("makeNextSelectedElementIds", () => {
const _makeNextSelectedElementIds = (
selectedElementIds: { [id: string]: true },
prevSelectedElementIds: { [id: string]: true },
expectUpdated: boolean,
) => {
const ret = makeNextSelectedElementIds(selectedElementIds, {
selectedElementIds: prevSelectedElementIds,
});
expect(ret === selectedElementIds).toBe(expectUpdated);
};
it("should return prevState selectedElementIds if no change", () => {
_makeNextSelectedElementIds({}, {}, false);
_makeNextSelectedElementIds({ 1: true }, { 1: true }, false);
_makeNextSelectedElementIds(
{ 1: true, 2: true },
{ 1: true, 2: true },
false,
);
});
it("should return new selectedElementIds if changed", () => {
// _makeNextSelectedElementIds({ 1: true }, { 1: false }, true);
_makeNextSelectedElementIds({ 1: true }, {}, true);
_makeNextSelectedElementIds({}, { 1: true }, true);
_makeNextSelectedElementIds({ 1: true }, { 2: true }, true);
_makeNextSelectedElementIds({ 1: true }, { 1: true, 2: true }, true);
_makeNextSelectedElementIds(
{ 1: true, 2: true },
{ 1: true, 3: true },
true,
);
});
});

View File

@@ -10,7 +10,6 @@ import {
getContainingFrame,
getFrameElements,
} from "../frame";
import { isShallowEqual } from "../utils";
/**
* Frames and their containing elements are not to be selected at the same time.
@@ -89,41 +88,11 @@ export const getElementsWithinSelection = (
return elementsInSelection;
};
// FIXME move this into the editor instance to keep utility methods stateless
export const isSomeElementSelected = (function () {
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
let lastSelectedElementIds: AppState["selectedElementIds"] | null = null;
let isSelected: boolean | null = null;
const ret = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds">,
): boolean => {
if (
isSelected != null &&
elements === lastElements &&
appState.selectedElementIds === lastSelectedElementIds
) {
return isSelected;
}
isSelected = elements.some(
(element) => appState.selectedElementIds[element.id],
);
lastElements = elements;
lastSelectedElementIds = appState.selectedElementIds;
return isSelected;
};
ret.clearCache = () => {
lastElements = null;
lastSelectedElementIds = null;
isSelected = null;
};
return ret;
})();
export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds">,
): boolean =>
elements.some((element) => appState.selectedElementIds[element.id]);
/**
* Returns common attribute (picked by `getAttribute` callback) of selected
@@ -192,18 +161,3 @@ export const getTargetElements = (
: getSelectedElements(elements, appState, {
includeBoundTextElement: true,
});
/**
* returns prevState's selectedElementids if no change from previous, so as to
* retain reference identity for memoization
*/
export const makeNextSelectedElementIds = (
nextSelectedElementIds: AppState["selectedElementIds"],
prevState: Pick<AppState, "selectedElementIds">,
) => {
if (isShallowEqual(prevState.selectedElementIds, nextSelectedElementIds)) {
return prevState.selectedElementIds;
}
return nextSelectedElementIds;
};

View File

@@ -314,12 +314,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -359,6 +353,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -506,12 +501,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -548,6 +537,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -704,12 +694,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -746,6 +730,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1076,12 +1061,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1118,6 +1097,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1448,12 +1428,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1490,6 +1464,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1646,12 +1621,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1686,6 +1655,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1881,12 +1851,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1923,6 +1887,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -2181,12 +2146,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -2220,6 +2179,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {
@@ -2228,6 +2188,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -2452,6 +2413,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {
"id3": true,
@@ -2569,12 +2531,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -2611,6 +2567,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -3447,12 +3404,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -3489,6 +3440,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -3819,12 +3771,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -3861,6 +3807,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -4191,12 +4138,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -4230,12 +4171,14 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -4456,6 +4399,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {
"id3": true,
@@ -4535,6 +4479,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {},
"viewBackgroundColor": "#ffffff",
@@ -4922,12 +4867,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -4953,6 +4892,7 @@ Object {
"pendingImageElementId": null,
"previousSelectedElementIds": Object {
"id0": true,
"id2": true,
},
"resizingElement": null,
"scrollX": 0,
@@ -4961,12 +4901,15 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -5501,12 +5444,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -5532,6 +5469,7 @@ Object {
"pendingImageElementId": null,
"previousSelectedElementIds": Object {
"id0": true,
"id2": true,
},
"resizingElement": null,
"scrollX": 0,
@@ -5540,6 +5478,8 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {
@@ -5548,6 +5488,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -5772,6 +5713,8 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedGroupIds": Object {
"id4": true,
@@ -6004,12 +5947,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6044,6 +5981,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -6399,12 +6337,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6441,6 +6373,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -6772,12 +6705,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6814,6 +6741,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,

File diff suppressed because it is too large Load Diff

View File

@@ -430,10 +430,7 @@ describe("arrow", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -449,10 +446,7 @@ describe("arrow", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -622,10 +616,7 @@ describe("line", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -641,10 +632,7 @@ describe("line", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -671,20 +659,14 @@ describe("freedraw", () => {
it("flips an unrotated drawing horizontally correctly", async () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkHorizontalFlip();
});
it("flips an unrotated drawing vertically correctly", async () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkVerticalFlip();
});
@@ -694,10 +676,7 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkRotatedHorizontalFlip(expectedAngle);
});
@@ -708,10 +687,7 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkRotatedVerticalFlip(expectedAngle);
});

View File

@@ -39,12 +39,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"isBindingEnabled": true,
@@ -76,6 +70,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": false,

View File

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

View File

@@ -55,7 +55,6 @@ export type Collaborator = {
avatarUrl?: string;
// user id. If supplied, we'll filter out duplicates when rendering user avatars.
id?: string;
socketId?: string;
};
export type DataURL = string & { _brand: "DataURL" };
@@ -116,12 +115,7 @@ export type AppState = {
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
suggestedBindings: SuggestedBinding[];
frameToHighlight: NonDeleted<ExcalidrawFrameElement> | null;
frameRendering: {
enabled: boolean;
name: boolean;
outline: boolean;
clip: boolean;
};
shouldRenderFrames: boolean;
editingFrame: string | null;
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
// element being edited, but not necessarily added to elements array yet
@@ -187,8 +181,8 @@ export type AppState = {
defaultSidebarDockedPreference: boolean;
lastPointerDownWith: PointerType;
selectedElementIds: Readonly<{ [id: string]: true }>;
previousSelectedElementIds: { [id: string]: true };
selectedElementIds: { [id: string]: boolean };
previousSelectedElementIds: { [id: string]: boolean };
selectedElementsAreBeingDragged: boolean;
shouldCacheIgnoreZoom: boolean;
toast: { message: string; closable?: boolean; duration?: number } | null;
@@ -377,12 +371,6 @@ export enum UserIdleState {
IDLE = "idle",
}
export enum PauseCollaborationState {
PAUSED = "paused",
RESUMED = "resumed",
SYNCED = "synced",
}
export type ExportOpts = {
saveFileToDisk?: boolean;
onExportToBackend?: (
@@ -555,7 +543,7 @@ export type ExcalidrawImperativeAPI = {
* the frames are still interactive in edit mode. As such, this API should be
* used in conjunction with view mode (props.viewModeEnabled).
*/
updateFrameRendering: InstanceType<typeof App>["updateFrameRendering"];
toggleFrameRendering: InstanceType<typeof App>["toggleFrameRendering"];
};
export type Device = Readonly<{

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:
"@sinonjs/commons" "^1.7.0"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -3173,6 +3168,11 @@ adjust-sourcemap-loader@^4.0.0:
loader-utils "^2.0.0"
regex-parser "^2.2.11"
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
integrity sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -3398,6 +3398,11 @@ array.prototype.tosorted@^1.1.1:
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3"
arraybuffer.slice@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@@ -3418,6 +3423,11 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@^2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
@@ -3610,6 +3620,11 @@ babel-preset-react-app@^10.0.1:
babel-plugin-macros "^3.1.0"
babel-plugin-transform-react-remove-prop-types "^0.4.24"
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -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"
integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==
base64-arraybuffer@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
integrity sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==
basic-auth@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
@@ -3652,6 +3672,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blob@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
bluebird@^3.5.5:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@@ -4052,6 +4077,21 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
component-bind@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
integrity sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==
component-emitter@~1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
integrity sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -4424,7 +4464,7 @@ debug@2.6.9, debug@^2.6.0:
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.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"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -4438,6 +4478,13 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
decimal.js@^10.2.1:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@@ -4771,21 +4818,33 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
engine.io-client@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91"
integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==
engine.io-client@~3.4.0:
version "3.4.4"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.4.tgz#77d8003f502b0782dd792b073a4d2cf7ca5ab967"
integrity sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.11.0"
xmlhttprequest-ssl "~2.0.0"
component-emitter "~1.3.0"
component-inherit "0.0.3"
debug "~3.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
parseqs "0.0.6"
parseuri "0.0.6"
ws "~6.1.0"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"
engine.io-parser@~5.0.3:
version "5.0.7"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.7.tgz#ed5eae76c71f398284c578ab6deafd3ba7e4e4f6"
integrity sha512-P+jDFbvK6lE3n1OL+q9KuzdOFWkkZ/cMV9gol/SbVfpyqfvrfrFTOFJ6fQm2VC3PZHlU3QPhVwmbsCnauHF2MQ==
engine.io-parser@~2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
dependencies:
after "0.8.2"
arraybuffer.slice "~0.0.7"
base64-arraybuffer "0.1.4"
blob "0.0.5"
has-binary2 "~1.0.2"
enhanced-resolve@^5.10.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"
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
has-binary2@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
dependencies:
isarray "2.0.1"
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
integrity sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -6180,6 +6251,11 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -6463,6 +6539,11 @@ is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
integrity sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@@ -8003,6 +8084,16 @@ parse5@6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parseqs@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
parseuri@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -9676,23 +9767,31 @@ sliced@^1.0.1:
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==
socket.io-client@4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab"
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
socket.io-client@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.1.tgz#91a4038ef4d03c19967bb3c646fec6e0eaa78cff"
integrity sha512-YXmXn3pA8abPOY//JtYxou95Ihvzmg8U6kQyolArkIyLd0pgVhrfor/iMsox8cn07WCOOvvuJ6XKegzIucPutQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.4.0"
socket.io-parser "~4.2.1"
backo2 "1.0.2"
component-bind "1.0.0"
component-emitter "~1.3.0"
debug "~3.1.0"
engine.io-client "~3.4.0"
has-binary2 "~1.0.2"
indexof "0.0.1"
parseqs "0.0.6"
parseuri "0.0.6"
socket.io-parser "~3.3.0"
to-array "0.1.4"
socket.io-parser@~4.2.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667"
integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==
socket.io-parser@~3.3.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
component-emitter "~1.3.0"
debug "~3.1.0"
isarray "2.0.1"
sockjs@^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"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
integrity sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -11049,10 +11153,12 @@ ws@^8.13.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
ws@~8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
ws@~6.1.0:
version "6.1.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
dependencies:
async-limiter "~1.0.0"
xml-name-validator@^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"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
xmlhttprequest-ssl@~1.5.4:
version "1.5.5"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha512-/bFPLUgJrfGUL10AIv4Y7/CUt6so9CLtB/oFxQSHseSDNNCdC6vwwKEqwLN6wNPBg9YWXAiMu8jkf6RPRS/75Q==
xmlhttprequest@1.8.0:
version "1.8.0"
@@ -11112,6 +11218,11 @@ yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"