narrow down & consolidate group selecting helper

This commit is contained in:
dwelle 2023-08-05 12:59:47 +02:00
parent 2f940fefc0
commit 40dccfcdd1
5 changed files with 194 additions and 188 deletions

View File

@ -263,8 +263,7 @@ const duplicateElements = (
...appState, ...appState,
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
...appState, editingGroupId: appState.editingGroupId,
selectedGroupIds: {},
selectedElementIds: nextElementsToSelect.reduce( selectedElementIds: nextElementsToSelect.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => { (acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) { if (!isBoundToContainer(element)) {

View File

@ -215,7 +215,7 @@ export const actionUngroup = register({
}); });
const updateAppState = selectGroupsForSelectedElements( const updateAppState = selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} }, appState,
getNonDeletedElements(nextElements), getNonDeletedElements(nextElements),
appState, appState,
null, null,

View File

@ -32,13 +32,6 @@ export const actionSelectAll = register({
...appState, ...appState,
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
...appState,
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
editingGroupId: null, editingGroupId: null,
selectedElementIds, selectedElementIds,
}, },
@ -46,6 +39,12 @@ export const actionSelectAll = register({
appState, appState,
app, app,
), ),
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
}, },
commitToHistory: true, commitToHistory: true,
}; };

View File

@ -177,7 +177,6 @@ import {
getSelectedGroupIds, getSelectedGroupIds,
isElementInGroup, isElementInGroup,
isSelectedViaGroup, isSelectedViaGroup,
selectGroups,
selectGroupsForSelectedElements, selectGroupsForSelectedElements,
} from "../groups"; } from "../groups";
import History from "../history"; import History from "../history";
@ -1706,7 +1705,7 @@ class App extends React.Component<AppProps, AppState> {
this.library.destroy(); this.library.destroy();
clearTimeout(touchTimeout); clearTimeout(touchTimeout);
isSomeElementSelected.clearCache(); isSomeElementSelected.clearCache();
selectGroups.clearCache(); selectGroupsForSelectedElements.clearCache();
touchTimeout = 0; touchTimeout = 0;
} }
@ -2300,35 +2299,37 @@ class App extends React.Component<AppProps, AppState> {
excludeElementsInFramesFromSelection(newElements); excludeElementsInFramesFromSelection(newElements);
this.setState( this.setState(
selectGroupsForSelectedElements( {
{ ...this.state,
...this.state, // keep sidebar (presumably the library) open if it's docked and
// keep sidebar (presumably the library) open if it's docked and // can fit.
// can fit. //
// // Note, we should close the sidebar only if we're dropping items
// Note, we should close the sidebar only if we're dropping items // from library, not when pasting from clipboard. Alas.
// from library, not when pasting from clipboard. Alas. openSidebar:
openSidebar: this.state.openSidebar &&
this.state.openSidebar && this.device.canDeviceFitSidebar &&
this.device.canDeviceFitSidebar && jotaiStore.get(isSidebarDockedAtom)
jotaiStore.get(isSidebarDockedAtom) ? this.state.openSidebar
? this.state.openSidebar : null,
: null, ...selectGroupsForSelectedElements(
selectedElementIds: nextElementsToSelect.reduce( {
(acc: Record<ExcalidrawElement["id"], true>, element) => { editingGroupId: null,
if (!isBoundToContainer(element)) { selectedElementIds: nextElementsToSelect.reduce(
acc[element.id] = true; (acc: Record<ExcalidrawElement["id"], true>, element) => {
} if (!isBoundToContainer(element)) {
return acc; acc[element.id] = true;
}, }
{}, return acc;
), },
selectedGroupIds: {}, {},
}, ),
this.scene.getNonDeletedElements(), },
this.state, this.scene.getNonDeletedElements(),
this, this.state,
), this,
),
},
() => { () => {
if (opts.files) { if (opts.files) {
this.addNewImagesToImageCache(); this.addNewImagesToImageCache();
@ -3589,19 +3590,18 @@ class App extends React.Component<AppProps, AppState> {
getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds); getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
if (selectedGroupId) { if (selectedGroupId) {
this.setState((prevState) => this.setState((prevState) => ({
selectGroupsForSelectedElements( ...prevState,
...selectGroupsForSelectedElements(
{ {
...prevState,
editingGroupId: selectedGroupId, editingGroupId: selectedGroupId,
selectedElementIds: { [hitElement!.id]: true }, selectedElementIds: { [hitElement!.id]: true },
selectedGroupIds: {},
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this, this,
), ),
); }));
return; return;
} }
} }
@ -5096,19 +5096,21 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
return selectGroupsForSelectedElements( return {
{ ...selectGroupsForSelectedElements(
...prevState, {
selectedElementIds: nextSelectedElementIds, editingGroupId: prevState.editingGroupId,
showHyperlinkPopup: selectedElementIds: nextSelectedElementIds,
hitElement.link || isEmbeddableElement(hitElement) },
? "info" this.scene.getNonDeletedElements(),
: false, prevState,
}, this,
this.scene.getNonDeletedElements(), ),
prevState, showHyperlinkPopup:
this, hitElement.link || isEmbeddableElement(hitElement)
); ? "info"
: false,
};
}); });
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
} }
@ -6028,34 +6030,36 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
return selectGroupsForSelectedElements( prevState = !shouldReuseSelection
{ ? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
...prevState, : prevState;
...(!shouldReuseSelection && {
selectedGroupIds: {}, return {
editingGroupId: null, ...selectGroupsForSelectedElements(
}), {
selectedElementIds: nextSelectedElementIds, editingGroupId: prevState.editingGroupId,
showHyperlinkPopup: selectedElementIds: nextSelectedElementIds,
elementsWithinSelection.length === 1 && },
(elementsWithinSelection[0].link || this.scene.getNonDeletedElements(),
isEmbeddableElement(elementsWithinSelection[0])) prevState,
? "info" this,
: false, ),
// select linear element only when we haven't box-selected anything else // select linear element only when we haven't box-selected anything else
selectedLinearElement: selectedLinearElement:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0]) isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor( ? new LinearElementEditor(
elementsWithinSelection[0], elementsWithinSelection[0],
this.scene, this.scene,
) )
: null, : null,
}, showHyperlinkPopup:
this.scene.getNonDeletedElements(), elementsWithinSelection.length === 1 &&
prevState, (elementsWithinSelection[0].link ||
this, isEmbeddableElement(elementsWithinSelection[0]))
); ? "info"
: false,
};
}); });
} }
} }
@ -6656,24 +6660,26 @@ class App extends React.Component<AppProps, AppState> {
{ selectedElementIds: newSelectedElementIds }, { selectedElementIds: newSelectedElementIds },
); );
return selectGroupsForSelectedElements( return {
{ ...selectGroupsForSelectedElements(
...prevState, {
selectedElementIds: newSelectedElementIds, editingGroupId: prevState.editingGroupId,
// set selectedLinearElement only if thats the only element selected selectedElementIds: newSelectedElementIds,
selectedLinearElement: },
newSelectedElements.length === 1 && this.scene.getNonDeletedElements(),
isLinearElement(newSelectedElements[0]) prevState,
? new LinearElementEditor( this,
newSelectedElements[0], ),
this.scene, // set selectedLinearElement only if thats the only element selected
) selectedLinearElement:
: prevState.selectedLinearElement, newSelectedElements.length === 1 &&
}, isLinearElement(newSelectedElements[0])
this.scene.getNonDeletedElements(), ? new LinearElementEditor(
prevState, newSelectedElements[0],
this, this.scene,
); )
: prevState.selectedLinearElement,
};
}); });
} }
} else if ( } else if (
@ -6701,19 +6707,21 @@ class App extends React.Component<AppProps, AppState> {
delete nextSelectedElementIds[element.id]; delete nextSelectedElementIds[element.id];
}); });
return selectGroupsForSelectedElements( return {
{ ...selectGroupsForSelectedElements(
...prevState, {
selectedElementIds: nextSelectedElementIds, editingGroupId: prevState.editingGroupId,
showHyperlinkPopup: selectedElementIds: nextSelectedElementIds,
hitElement.link || isEmbeddableElement(hitElement) },
? "info" this.scene.getNonDeletedElements(),
: false, prevState,
}, this,
this.scene.getNonDeletedElements(), ),
prevState, showHyperlinkPopup:
this, hitElement.link || isEmbeddableElement(hitElement)
); ? "info"
: false,
};
}); });
} else { } else {
// add element to selection while keeping prev elements selected // add element to selection while keeping prev elements selected
@ -6731,20 +6739,20 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => ({ this.setState((prevState) => ({
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
...prevState, editingGroupId: prevState.editingGroupId,
selectedElementIds: { [hitElement.id]: true }, selectedElementIds: { [hitElement.id]: true },
selectedLinearElement:
isLinearElement(hitElement) &&
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement, this.scene)
: prevState.selectedLinearElement,
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this, this,
), ),
selectedLinearElement:
isLinearElement(hitElement) &&
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement, this.scene)
: prevState.selectedLinearElement,
})); }));
} }
} }
@ -7595,16 +7603,16 @@ class App extends React.Component<AppProps, AppState> {
...this.state, ...this.state,
...selectGroupsForSelectedElements( ...selectGroupsForSelectedElements(
{ {
...this.state, editingGroupId: this.state.editingGroupId,
selectedElementIds: { [element.id]: true }, selectedElementIds: { [element.id]: true },
selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element, this.scene)
: null,
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
this, this,
), ),
selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element, this.scene)
: null,
} }
: this.state), : this.state),
showHyperlinkPopup: false, showHyperlinkPopup: false,

View File

@ -55,25 +55,29 @@ export const selectGroup = (
}; };
}; };
export const selectGroups = (function () { export const selectGroupsForSelectedElements = (function () {
type SelectGroupsReturnType = Pick<
InteractiveCanvasAppState,
"selectedGroupIds" | "editingGroupId" | "selectedElementIds"
>;
let lastSelectedElements: readonly NonDeleted<ExcalidrawElement>[] | null = let lastSelectedElements: readonly NonDeleted<ExcalidrawElement>[] | null =
null; null;
let lastElements: readonly NonDeleted<ExcalidrawElement>[] | null = null; let lastElements: readonly NonDeleted<ExcalidrawElement>[] | null = null;
let lastAppState: InteractiveCanvasAppState | null = null; let lastReturnValue: SelectGroupsReturnType | null = null;
const ret = ( const _selectGroups = (
selectedElements: readonly NonDeleted<ExcalidrawElement>[], selectedElements: readonly NonDeleted<ExcalidrawElement>[],
elements: readonly NonDeleted<ExcalidrawElement>[], elements: readonly NonDeleted<ExcalidrawElement>[],
appState: InteractiveCanvasAppState, appState: Pick<AppState, "selectedElementIds" | "editingGroupId">,
): InteractiveCanvasAppState => { ): SelectGroupsReturnType => {
if ( if (
lastAppState !== undefined && lastReturnValue !== undefined &&
elements === lastElements && elements === lastElements &&
selectedElements === lastSelectedElements && selectedElements === lastSelectedElements &&
appState.editingGroupId === lastAppState?.editingGroupId && appState.editingGroupId === lastReturnValue?.editingGroupId
appState.selectedGroupIds === lastAppState?.selectedGroupIds
) { ) {
return lastAppState; return lastReturnValue;
} }
const selectedGroupIds: Record<GroupId, boolean> = {}; const selectedGroupIds: Record<GroupId, boolean> = {};
@ -126,8 +130,8 @@ export const selectGroups = (function () {
lastElements = elements; lastElements = elements;
lastSelectedElements = selectedElements; lastSelectedElements = selectedElements;
lastAppState = { lastReturnValue = {
...appState, editingGroupId: appState.editingGroupId,
selectedGroupIds, selectedGroupIds,
selectedElementIds: { selectedElementIds: {
...appState.selectedElementIds, ...appState.selectedElementIds,
@ -135,16 +139,55 @@ export const selectGroups = (function () {
}, },
}; };
return lastAppState; return lastReturnValue;
}; };
ret.clearCache = () => { /**
* When you select an element, you often want to actually select the whole group it's in, unless
* you're currently editing that group.
*/
const selectGroupsForSelectedElements = (
appState: Pick<AppState, "selectedElementIds" | "editingGroupId">,
elements: readonly NonDeletedExcalidrawElement[],
prevAppState: InteractiveCanvasAppState,
/**
* supply null in cases where you don't have access to App instance and
* you don't care about optimizing selectElements retrieval
*/
app: AppClassProperties | null,
): Pick<
InteractiveCanvasAppState,
"selectedGroupIds" | "editingGroupId" | "selectedElementIds"
> => {
const selectedElements = app
? app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements,
})
: getSelectedElements(elements, appState);
if (!selectedElements.length) {
return {
selectedGroupIds: {},
editingGroupId: null,
selectedElementIds: makeNextSelectedElementIds(
appState.selectedElementIds,
prevAppState,
),
};
}
return _selectGroups(selectedElements, elements, appState);
};
selectGroupsForSelectedElements.clearCache = () => {
lastElements = null; lastElements = null;
lastSelectedElements = null; lastSelectedElements = null;
lastAppState = null; lastReturnValue = null;
}; };
return ret; return selectGroupsForSelectedElements;
})(); })();
/** /**
@ -171,49 +214,6 @@ export const getSelectedGroupIds = (
.filter(([groupId, isSelected]) => isSelected) .filter(([groupId, isSelected]) => isSelected)
.map(([groupId, isSelected]) => groupId); .map(([groupId, isSelected]) => groupId);
/**
* When you select an element, you often want to actually select the whole group it's in, unless
* you're currently editing that group.
*/
export const selectGroupsForSelectedElements = (
appState: InteractiveCanvasAppState,
elements: readonly NonDeletedExcalidrawElement[],
prevAppState: InteractiveCanvasAppState,
/**
* supply null in cases where you don't have access to App instance and
* you don't care about optimizing selectElements retrieval
*/
app: AppClassProperties | null,
): InteractiveCanvasAppState => {
let nextAppState: InteractiveCanvasAppState = {
...appState,
selectedGroupIds: {},
};
const selectedElements = app
? app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements,
})
: getSelectedElements(elements, appState);
if (!selectedElements.length) {
return {
...nextAppState,
editingGroupId: null,
selectedElementIds: makeNextSelectedElementIds(
nextAppState.selectedElementIds,
prevAppState,
),
};
}
nextAppState = selectGroups(selectedElements, elements, appState);
return nextAppState;
};
// given a list of elements, return the the actual group ids that should be selected // given a list of elements, return the the actual group ids that should be selected
// or used to update the elements // or used to update the elements
export const selectGroupsFromGivenElements = ( export const selectGroupsFromGivenElements = (