210 lines
4.7 KiB
TypeScript
210 lines
4.7 KiB
TypeScript
import { useCallback, useEffect, useMemo } from "react";
|
|
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
|
|
|
function mergeUniqueIds({ ids }: { ids: string[] }) {
|
|
return [...new Set(ids)];
|
|
}
|
|
|
|
function getRangeIds({
|
|
orderedIds,
|
|
anchorId,
|
|
targetId,
|
|
}: {
|
|
orderedIds: string[];
|
|
anchorId: string;
|
|
targetId: string;
|
|
}) {
|
|
const anchorIndex = orderedIds.indexOf(anchorId);
|
|
const targetIndex = orderedIds.indexOf(targetId);
|
|
|
|
if (anchorIndex === -1 || targetIndex === -1) {
|
|
return [targetId];
|
|
}
|
|
|
|
const rangeStart = Math.min(anchorIndex, targetIndex);
|
|
const rangeEnd = Math.max(anchorIndex, targetIndex);
|
|
return orderedIds.slice(rangeStart, rangeEnd + 1);
|
|
}
|
|
|
|
export function useAssetsSelection({
|
|
orderedIds,
|
|
}: {
|
|
orderedIds: string[];
|
|
}) {
|
|
const {
|
|
selectedMediaIds,
|
|
selectionAnchorId,
|
|
setMediaSelection,
|
|
clearMediaSelection,
|
|
} = useAssetsPanelStore();
|
|
const selectedIdSet = useMemo(() => {
|
|
return new Set(selectedMediaIds);
|
|
}, [selectedMediaIds]);
|
|
|
|
useEffect(() => {
|
|
const validIds = new Set(orderedIds);
|
|
const nextSelectedIds = selectedMediaIds.filter((id) => validIds.has(id));
|
|
const nextAnchorId =
|
|
selectionAnchorId && validIds.has(selectionAnchorId)
|
|
? selectionAnchorId
|
|
: nextSelectedIds[nextSelectedIds.length - 1] ?? null;
|
|
const isSelectionUnchanged =
|
|
nextSelectedIds.length === selectedMediaIds.length &&
|
|
nextAnchorId === selectionAnchorId;
|
|
|
|
if (isSelectionUnchanged) {
|
|
return;
|
|
}
|
|
|
|
setMediaSelection({
|
|
ids: nextSelectedIds,
|
|
anchorId: nextAnchorId,
|
|
});
|
|
}, [orderedIds, selectedMediaIds, selectionAnchorId, setMediaSelection]);
|
|
|
|
const replaceSelection = useCallback(
|
|
(ids: string[], anchorId?: string | null) => {
|
|
setMediaSelection({
|
|
ids: mergeUniqueIds({ ids }),
|
|
anchorId,
|
|
});
|
|
},
|
|
[setMediaSelection],
|
|
);
|
|
|
|
const isSelected = useCallback(
|
|
(id: string) => {
|
|
return selectedIdSet.has(id);
|
|
},
|
|
[selectedIdSet],
|
|
);
|
|
|
|
const toggleSelection = useCallback(
|
|
(id: string) => {
|
|
if (selectedIdSet.has(id)) {
|
|
const nextSelectedIds = selectedMediaIds.filter(
|
|
(selectedId) => selectedId !== id,
|
|
);
|
|
const nextAnchorId =
|
|
selectionAnchorId === id
|
|
? nextSelectedIds[nextSelectedIds.length - 1] ?? null
|
|
: selectionAnchorId;
|
|
replaceSelection(nextSelectedIds, nextAnchorId);
|
|
return;
|
|
}
|
|
|
|
replaceSelection([...selectedMediaIds, id], id);
|
|
},
|
|
[
|
|
selectedIdSet,
|
|
selectedMediaIds,
|
|
selectionAnchorId,
|
|
replaceSelection,
|
|
],
|
|
);
|
|
|
|
const selectRange = useCallback(
|
|
({
|
|
targetId,
|
|
isAdditive,
|
|
}: {
|
|
targetId: string;
|
|
isAdditive: boolean;
|
|
}) => {
|
|
const anchorId =
|
|
selectionAnchorId ??
|
|
selectedMediaIds[selectedMediaIds.length - 1] ??
|
|
targetId;
|
|
const rangeIds = getRangeIds({
|
|
orderedIds,
|
|
anchorId,
|
|
targetId,
|
|
});
|
|
const nextSelectedIds = isAdditive
|
|
? mergeUniqueIds({
|
|
ids: [...selectedMediaIds, ...rangeIds],
|
|
})
|
|
: rangeIds;
|
|
|
|
replaceSelection(nextSelectedIds, anchorId);
|
|
},
|
|
[orderedIds, replaceSelection, selectedMediaIds, selectionAnchorId],
|
|
);
|
|
|
|
const handleItemClick = useCallback(
|
|
({
|
|
event,
|
|
id,
|
|
}: {
|
|
event: React.MouseEvent;
|
|
id: string;
|
|
}) => {
|
|
const isToggleSelection = event.ctrlKey || event.metaKey;
|
|
|
|
if (event.shiftKey) {
|
|
selectRange({
|
|
targetId: id,
|
|
isAdditive: isToggleSelection,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isToggleSelection) {
|
|
toggleSelection(id);
|
|
return;
|
|
}
|
|
|
|
replaceSelection([id], id);
|
|
},
|
|
[replaceSelection, selectRange, toggleSelection],
|
|
);
|
|
|
|
const handleItemContextMenu = useCallback(
|
|
(id: string) => {
|
|
if (selectedIdSet.has(id)) {
|
|
return;
|
|
}
|
|
|
|
replaceSelection([id], id);
|
|
},
|
|
[selectedIdSet, replaceSelection],
|
|
);
|
|
|
|
const applyBoxSelection = useCallback(
|
|
({
|
|
intersectedIds,
|
|
initialSelectedIds,
|
|
initialAnchorId,
|
|
isAdditive,
|
|
}: {
|
|
intersectedIds: string[];
|
|
initialSelectedIds: string[];
|
|
initialAnchorId: string | null;
|
|
isAdditive: boolean;
|
|
}) => {
|
|
const nextSelectedIds = isAdditive
|
|
? mergeUniqueIds({
|
|
ids: [...initialSelectedIds, ...intersectedIds],
|
|
})
|
|
: intersectedIds;
|
|
const nextAnchorId = isAdditive
|
|
? initialAnchorId ?? intersectedIds[intersectedIds.length - 1] ?? null
|
|
: intersectedIds[intersectedIds.length - 1] ?? null;
|
|
|
|
replaceSelection(nextSelectedIds, nextAnchorId);
|
|
},
|
|
[replaceSelection],
|
|
);
|
|
|
|
return {
|
|
selectedMediaIds,
|
|
selectionAnchorId,
|
|
isSelected,
|
|
replaceSelection,
|
|
clearSelection: clearMediaSelection,
|
|
handleItemClick,
|
|
handleItemContextMenu,
|
|
applyBoxSelection,
|
|
};
|
|
}
|