mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-03 22:29:55 +02:00
fix: support bidirectional shift+click selection in library items (#10034)
* fix: support bidirectional shift+click selection in library items - Enable bottom-up multi-selection (previously only top-down worked) - Use Math.min/max to handle selection range in both directions - Maintains existing behavior for preserving non-contiguous selections - Fixes issue where shift+clicking items above last selected item failed * improve deselection behavior --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -281,19 +281,29 @@ export const LibraryMenu = memo(() => {
|
|||||||
if (target.closest(`.${CLASSES.SIDEBAR}`)) {
|
if (target.closest(`.${CLASSES.SIDEBAR}`)) {
|
||||||
// stop propagation so that we don't prevent it downstream
|
// stop propagation so that we don't prevent it downstream
|
||||||
// (default browser behavior is to clear search input on ESC)
|
// (default browser behavior is to clear search input on ESC)
|
||||||
event.stopPropagation();
|
|
||||||
if (selectedItems.length > 0) {
|
if (selectedItems.length > 0) {
|
||||||
|
event.stopPropagation();
|
||||||
setSelectedItems([]);
|
setSelectedItems([]);
|
||||||
} else if (
|
} else if (
|
||||||
isWritableElement(target) &&
|
isWritableElement(target) &&
|
||||||
target instanceof HTMLInputElement &&
|
target instanceof HTMLInputElement &&
|
||||||
!target.value
|
!target.value
|
||||||
) {
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
// if search input empty -> close library
|
// if search input empty -> close library
|
||||||
// (maybe not a good idea?)
|
// (maybe not a good idea?)
|
||||||
setAppState({ openSidebar: null });
|
setAppState({ openSidebar: null });
|
||||||
app.focusContainer();
|
app.focusContainer();
|
||||||
}
|
}
|
||||||
|
} else if (selectedItems.length > 0) {
|
||||||
|
const { x, y } = app.lastViewportPosition;
|
||||||
|
const elementUnderCursor = document.elementFromPoint(x, y);
|
||||||
|
// also deselect elements if sidebar doesn't have focus but the
|
||||||
|
// cursor is over it
|
||||||
|
if (elementUnderCursor?.closest(`.${CLASSES.SIDEBAR}`)) {
|
||||||
|
event.stopPropagation();
|
||||||
|
setSelectedItems([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -138,10 +138,13 @@ export default function LibraryMenuItems({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedItemsMap = arrayToMap(selectedItems);
|
const selectedItemsMap = arrayToMap(selectedItems);
|
||||||
|
// Support both top-down and bottom-up selection by using min/max
|
||||||
|
const minRange = Math.min(rangeStart, rangeEnd);
|
||||||
|
const maxRange = Math.max(rangeStart, rangeEnd);
|
||||||
const nextSelectedIds = orderedItems.reduce(
|
const nextSelectedIds = orderedItems.reduce(
|
||||||
(acc: LibraryItem["id"][], item, idx) => {
|
(acc: LibraryItem["id"][], item, idx) => {
|
||||||
if (
|
if (
|
||||||
(idx >= rangeStart && idx <= rangeEnd) ||
|
(idx >= minRange && idx <= maxRange) ||
|
||||||
selectedItemsMap.has(item.id)
|
selectedItemsMap.has(item.id)
|
||||||
) {
|
) {
|
||||||
acc.push(item.id);
|
acc.push(item.id);
|
||||||
@@ -169,6 +172,14 @@ export default function LibraryMenuItems({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if selection is removed (e.g. via esc), reset last selected item
|
||||||
|
// so that subsequent shift+clicks don't select a large range
|
||||||
|
if (!selectedItems.length) {
|
||||||
|
setLastSelectedItem(null);
|
||||||
|
}
|
||||||
|
}, [selectedItems]);
|
||||||
|
|
||||||
const getInsertedElements = useCallback(
|
const getInsertedElements = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
let targetElements;
|
let targetElements;
|
||||||
|
Reference in New Issue
Block a user