feat: make appState.selectedElementIds more stable (#6745)

This commit is contained in:
David Luzar 2023-07-08 23:33:34 +02:00 committed by GitHub
parent 3ddcc48e4c
commit 49e4289878
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 503 additions and 295 deletions

View file

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

View file

@ -10,6 +10,7 @@ import {
getContainingFrame,
getFrameElements,
} from "../frame";
import { isShallowEqual } from "../utils";
/**
* Frames and their containing elements are not to be selected at the same time.
@ -88,11 +89,41 @@ export const getElementsWithinSelection = (
return elementsInSelection;
};
export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds">,
): boolean =>
elements.some((element) => appState.selectedElementIds[element.id]);
// 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;
})();
/**
* Returns common attribute (picked by `getAttribute` callback) of selected
@ -161,3 +192,18 @@ 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;
};