mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-02 13:49:57 +02:00
Compare commits
1 Commits
arnost/col
...
dwelle/upd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b8da178ebd |
@@ -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",
|
||||
|
@@ -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)) {
|
||||
|
@@ -274,7 +274,6 @@ const duplicateElements = (
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
appState,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@@ -125,6 +125,13 @@ export const actionFinalize = register({
|
||||
{ x, y },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!appState.activeTool.locked &&
|
||||
appState.activeTool.type !== "freedraw"
|
||||
) {
|
||||
appState.selectedElementIds[multiPointElement.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
@@ -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({
|
||||
|
@@ -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,
|
||||
|
@@ -41,7 +41,6 @@ export const actionSelectAll = register({
|
||||
selectedElementIds,
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
),
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
@@ -119,7 +119,7 @@ export type ActionName =
|
||||
| "toggleHandTool"
|
||||
| "selectAllElementsInFrame"
|
||||
| "removeAllElementsFromFrame"
|
||||
| "updateFrameRendering"
|
||||
| "toggleFrameRendering"
|
||||
| "setFrameAsActiveTool"
|
||||
| "createContainerFromText"
|
||||
| "wrapTextInContainer";
|
||||
|
@@ -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 },
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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}
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
@@ -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() {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
@@ -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];
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
},
|
||||
),
|
||||
|
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
@@ -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;
|
||||
};
|
||||
|
@@ -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
@@ -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);
|
||||
});
|
||||
|
@@ -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,
|
||||
|
@@ -89,7 +89,6 @@ const populateElements = (
|
||||
...selectGroupsForSelectedElements(
|
||||
{ ...h.state, ...appState, selectedElementIds },
|
||||
h.elements,
|
||||
h.state,
|
||||
),
|
||||
...appState,
|
||||
selectedElementIds,
|
||||
|
20
src/types.ts
20
src/types.ts
@@ -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<{
|
||||
|
11
src/utils.ts
11
src/utils.ts
@@ -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
193
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user