narrow down & consolidate group selecting helper
This commit is contained in:
parent
2f940fefc0
commit
40dccfcdd1
@ -263,8 +263,7 @@ const duplicateElements = (
|
||||
...appState,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
...appState,
|
||||
selectedGroupIds: {},
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
|
@ -215,7 +215,7 @@ export const actionUngroup = register({
|
||||
});
|
||||
|
||||
const updateAppState = selectGroupsForSelectedElements(
|
||||
{ ...appState, selectedGroupIds: {} },
|
||||
appState,
|
||||
getNonDeletedElements(nextElements),
|
||||
appState,
|
||||
null,
|
||||
|
@ -32,13 +32,6 @@ export const actionSelectAll = register({
|
||||
...appState,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
...appState,
|
||||
selectedLinearElement:
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
: null,
|
||||
editingGroupId: null,
|
||||
selectedElementIds,
|
||||
},
|
||||
@ -46,6 +39,12 @@ export const actionSelectAll = register({
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
selectedLinearElement:
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
: null,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
@ -177,7 +177,6 @@ import {
|
||||
getSelectedGroupIds,
|
||||
isElementInGroup,
|
||||
isSelectedViaGroup,
|
||||
selectGroups,
|
||||
selectGroupsForSelectedElements,
|
||||
} from "../groups";
|
||||
import History from "../history";
|
||||
@ -1706,7 +1705,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.library.destroy();
|
||||
clearTimeout(touchTimeout);
|
||||
isSomeElementSelected.clearCache();
|
||||
selectGroups.clearCache();
|
||||
selectGroupsForSelectedElements.clearCache();
|
||||
touchTimeout = 0;
|
||||
}
|
||||
|
||||
@ -2300,35 +2299,37 @@ class App extends React.Component<AppProps, AppState> {
|
||||
excludeElementsInFramesFromSelection(newElements);
|
||||
|
||||
this.setState(
|
||||
selectGroupsForSelectedElements(
|
||||
{
|
||||
...this.state,
|
||||
// keep sidebar (presumably the library) open if it's docked and
|
||||
// can fit.
|
||||
//
|
||||
// Note, we should close the sidebar only if we're dropping items
|
||||
// from library, not when pasting from clipboard. Alas.
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.canDeviceFitSidebar &&
|
||||
jotaiStore.get(isSidebarDockedAtom)
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
selectedGroupIds: {},
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
this,
|
||||
),
|
||||
{
|
||||
...this.state,
|
||||
// keep sidebar (presumably the library) open if it's docked and
|
||||
// can fit.
|
||||
//
|
||||
// Note, we should close the sidebar only if we're dropping items
|
||||
// from library, not when pasting from clipboard. Alas.
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.canDeviceFitSidebar &&
|
||||
jotaiStore.get(isSidebarDockedAtom)
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: null,
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
this,
|
||||
),
|
||||
},
|
||||
() => {
|
||||
if (opts.files) {
|
||||
this.addNewImagesToImageCache();
|
||||
@ -3589,19 +3590,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
|
||||
|
||||
if (selectedGroupId) {
|
||||
this.setState((prevState) =>
|
||||
selectGroupsForSelectedElements(
|
||||
this.setState((prevState) => ({
|
||||
...prevState,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
editingGroupId: selectedGroupId,
|
||||
selectedElementIds: { [hitElement!.id]: true },
|
||||
selectedGroupIds: {},
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -5096,19 +5096,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
showHyperlinkPopup:
|
||||
hitElement.link || isEmbeddableElement(hitElement)
|
||||
? "info"
|
||||
: false,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
return {
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
showHyperlinkPopup:
|
||||
hitElement.link || isEmbeddableElement(hitElement)
|
||||
? "info"
|
||||
: false,
|
||||
};
|
||||
});
|
||||
pointerDownState.hit.wasAddedToSelection = true;
|
||||
}
|
||||
@ -6028,34 +6030,36 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
...(!shouldReuseSelection && {
|
||||
selectedGroupIds: {},
|
||||
editingGroupId: null,
|
||||
}),
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
showHyperlinkPopup:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
(elementsWithinSelection[0].link ||
|
||||
isEmbeddableElement(elementsWithinSelection[0]))
|
||||
? "info"
|
||||
: false,
|
||||
// select linear element only when we haven't box-selected anything else
|
||||
selectedLinearElement:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
isLinearElement(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene,
|
||||
)
|
||||
: null,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
prevState = !shouldReuseSelection
|
||||
? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
|
||||
: prevState;
|
||||
|
||||
return {
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
// select linear element only when we haven't box-selected anything else
|
||||
selectedLinearElement:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
isLinearElement(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene,
|
||||
)
|
||||
: null,
|
||||
showHyperlinkPopup:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
(elementsWithinSelection[0].link ||
|
||||
isEmbeddableElement(elementsWithinSelection[0]))
|
||||
? "info"
|
||||
: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6656,24 +6660,26 @@ class App extends React.Component<AppProps, AppState> {
|
||||
{ selectedElementIds: newSelectedElementIds },
|
||||
);
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
selectedElementIds: newSelectedElementIds,
|
||||
// set selectedLinearElement only if thats the only element selected
|
||||
selectedLinearElement:
|
||||
newSelectedElements.length === 1 &&
|
||||
isLinearElement(newSelectedElements[0])
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene,
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
return {
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
selectedElementIds: newSelectedElementIds,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
// set selectedLinearElement only if thats the only element selected
|
||||
selectedLinearElement:
|
||||
newSelectedElements.length === 1 &&
|
||||
isLinearElement(newSelectedElements[0])
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene,
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
@ -6701,19 +6707,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
delete nextSelectedElementIds[element.id];
|
||||
});
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
showHyperlinkPopup:
|
||||
hitElement.link || isEmbeddableElement(hitElement)
|
||||
? "info"
|
||||
: false,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
);
|
||||
return {
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
selectedElementIds: nextSelectedElementIds,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
this,
|
||||
),
|
||||
showHyperlinkPopup:
|
||||
hitElement.link || isEmbeddableElement(hitElement)
|
||||
? "info"
|
||||
: false,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// add element to selection while keeping prev elements selected
|
||||
@ -6731,20 +6739,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState((prevState) => ({
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
editingGroupId: prevState.editingGroupId,
|
||||
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(),
|
||||
prevState,
|
||||
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,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
...this.state,
|
||||
editingGroupId: this.state.editingGroupId,
|
||||
selectedElementIds: { [element.id]: true },
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element, this.scene)
|
||||
: null,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
this,
|
||||
),
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element, this.scene)
|
||||
: null,
|
||||
}
|
||||
: this.state),
|
||||
showHyperlinkPopup: false,
|
||||
|
116
src/groups.ts
116
src/groups.ts
@ -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 =
|
||||
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>[],
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||
appState: InteractiveCanvasAppState,
|
||||
): InteractiveCanvasAppState => {
|
||||
appState: Pick<AppState, "selectedElementIds" | "editingGroupId">,
|
||||
): SelectGroupsReturnType => {
|
||||
if (
|
||||
lastAppState !== undefined &&
|
||||
lastReturnValue !== undefined &&
|
||||
elements === lastElements &&
|
||||
selectedElements === lastSelectedElements &&
|
||||
appState.editingGroupId === lastAppState?.editingGroupId &&
|
||||
appState.selectedGroupIds === lastAppState?.selectedGroupIds
|
||||
appState.editingGroupId === lastReturnValue?.editingGroupId
|
||||
) {
|
||||
return lastAppState;
|
||||
return lastReturnValue;
|
||||
}
|
||||
|
||||
const selectedGroupIds: Record<GroupId, boolean> = {};
|
||||
@ -126,8 +130,8 @@ export const selectGroups = (function () {
|
||||
lastElements = elements;
|
||||
lastSelectedElements = selectedElements;
|
||||
|
||||
lastAppState = {
|
||||
...appState,
|
||||
lastReturnValue = {
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedGroupIds,
|
||||
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;
|
||||
lastSelectedElements = null;
|
||||
lastAppState = null;
|
||||
lastReturnValue = null;
|
||||
};
|
||||
|
||||
return ret;
|
||||
return selectGroupsForSelectedElements;
|
||||
})();
|
||||
|
||||
/**
|
||||
@ -171,49 +214,6 @@ export const getSelectedGroupIds = (
|
||||
.filter(([groupId, isSelected]) => isSelected)
|
||||
.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
|
||||
// or used to update the elements
|
||||
export const selectGroupsFromGivenElements = (
|
||||
|
Loading…
x
Reference in New Issue
Block a user