selection

This commit is contained in:
2025-11-01 20:28:00 +01:00
parent 9559a1b5e2
commit f6ceff5444
3 changed files with 493 additions and 297 deletions
+115 -297
View File
@@ -19,6 +19,8 @@ import { AppShellContext } from '../appShellContext';
import DropOverlay from './DropOverlay';
import { useManagementModals } from './useManagementModals';
import { api, useAppDispatch, useAppState } from './appState';
import { useDetailPanel } from './useDetailPanel';
import { useDocumentSelection } from './useDocumentSelection';
const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
@@ -204,15 +206,12 @@ const AppLayout = () => {
}
}, [documentsViewMode]);
const initialRowSelection = [];
const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection);
const [selectionOrder, setSelectionOrder] = useState(initialRowSelection);
const [focusedDocumentId, setFocusedDocumentId] = useState(null);
const [focusedRowKey, setFocusedRowKey] = useState(null);
const tokenRef = useRef(token);
const refreshPromiseRef = useRef(null);
const breadcrumbFetchRef = useRef(new Set());
const tagRemovalCursorActiveRef = useRef(false);
const tenantIdRef = useRef(currentTenantId);
const detailPanelControlRef = useRef({ open: () => {}, close: () => {} });
const setTagRemovalCursor = useCallback((active) => {
if (typeof document === 'undefined') {
return;
@@ -352,17 +351,40 @@ const AppLayout = () => {
}
const tagManager = tagManagerRef.current;
const {
selectedRowKeys,
setSelectedRowKeys,
selectionOrder,
setSelectionOrder,
selectionOrderRef,
selectionAnchorRef,
selectionInitializedRef,
focusedDocumentId,
setFocusedDocumentId,
focusedRowKey,
setFocusedRowKey,
applySelection,
clearSelection: clearSelectionInternal,
handleRowSelection: handleRowSelectionInternal,
promoteSelectionOrder: promoteSelectionOrderInternal,
configureSelectionEnvironment,
} = useDocumentSelection({
resolveDocumentRowKey,
resolveFolderRowKey,
isDocumentRowKey,
isFolderRowKey,
getRowId,
initialSelection: initialRowSelection,
});
const getDocumentAsset = useCallback((doc, type) => {
if (!doc || !type) return null;
return getAssetFromVersion(doc.current_version || null, type);
}, []);
const bootstrapInitializedRef = useRef(false);
const selectionInitializedRef = useRef(false);
const dragCounterRef = useRef(0);
const detailFolderFetchRef = useRef(new Set());
const selectionAnchorRef = useRef(initialRowSelection[initialRowSelection.length - 1] || null);
const selectionOrderRef = useRef(initialRowSelection);
const selectedDocumentIds = useMemo(
() =>
@@ -384,9 +406,6 @@ const AppLayout = () => {
[selectedRowKeys],
);
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
const resetWorkspaceState = useCallback(() => {
const rootNode = createRootNode();
@@ -416,9 +435,7 @@ const AppLayout = () => {
setActiveCorrespondentFilters([]);
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
setActivePreviewId(null);
setDetailPanelOpen(false);
setDetailPanelDocIds([]);
setDetailPanelDocs([]);
detailPanelControlRef.current.close();
assetManager.reset();
setPreviewEntries(() => new Map());
previewInflightRef.current = new Map();
@@ -428,7 +445,15 @@ const AppLayout = () => {
bootstrapInitializedRef.current = false;
selectionInitializedRef.current = false;
tenantIdRef.current = null;
}, [assetManager]);
}, [
assetManager,
selectionAnchorRef,
selectionInitializedRef,
selectionOrderRef,
setFocusedDocumentId,
setSelectedRowKeys,
setSelectionOrder,
]);
const tagLookupById = useMemo(() => {
const map = new Map();
@@ -449,37 +474,6 @@ const AppLayout = () => {
});
return map;
}, [correspondents]);
const updateSelectionOrder = useCallback((nextSelection, interactedKeys = []) => {
const nextSet = new Set(nextSelection);
const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id));
const interacted = (interactedKeys || []).filter((id, index, array) => array.indexOf(id) === index);
const base = previousOrder.filter((id) => !interacted.includes(id));
const result = [...base];
interacted.forEach((id) => {
if (nextSet.has(id) && !result.includes(id)) {
result.push(id);
}
});
nextSelection.forEach((id) => {
if (!result.includes(id)) {
result.push(id);
}
});
if (
result.length !== selectionOrderRef.current.length ||
result.some((id, index) => selectionOrderRef.current[index] !== id)
) {
selectionOrderRef.current = result;
setSelectionOrder(result);
} else {
selectionOrderRef.current = result;
}
}, []);
useEffect(() => {
if (appStatus === 'logged-out' || appStatus === 'selecting-tenant') {
resetWorkspaceState();
@@ -573,7 +567,7 @@ const AppLayout = () => {
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
}
selectionInitializedRef.current = true;
}, [selectedDocumentIds, activePreviewId]);
}, [selectedDocumentIds, activePreviewId, selectionInitializedRef]);
const currentFolderName = useMemo(() => {
if (selectedFolder === 'root' || !currentFolder) return DEFAULT_FOLDER_NAME;
@@ -648,7 +642,16 @@ const AppLayout = () => {
return nextFocus;
},
[assetManager, focusedDocumentId, setSelectionOrder],
[
assetManager,
focusedDocumentId,
selectionAnchorRef,
selectionInitializedRef,
selectionOrderRef,
setFocusedDocumentId,
setSelectedRowKeys,
setSelectionOrder,
],
);
const showingSearchResults = searchResults !== null;
@@ -821,82 +824,6 @@ const AppLayout = () => {
[setDocuments, setSearchResults, setFolderContents],
);
const applySelection = useCallback(
(rowKeys, { anchor, interactedKeys = [] } = {}) => {
const unique = [];
(rowKeys || []).forEach((key) => {
if (!key) return;
const canonicalKey = visibleRowKeySet.has(key)
? key
: isDocumentRowKey(key)
? resolveDocumentRowKey(getRowId(key))
: isFolderRowKey(key)
? resolveFolderRowKey(getRowId(key))
: null;
if (!canonicalKey || !visibleRowKeySet.has(canonicalKey)) {
return;
}
if (!unique.includes(canonicalKey)) {
unique.push(canonicalKey);
}
});
let resolvedAnchor = anchor;
if (resolvedAnchor && !unique.includes(resolvedAnchor)) {
resolvedAnchor = null;
}
setSelectedRowKeys(unique);
updateSelectionOrder(unique, interactedKeys);
const nextFocusedDocumentId = (() => {
if (focusedDocumentId) {
const focusKey = resolveDocumentRowKey(focusedDocumentId);
if (focusKey && unique.includes(focusKey)) {
return focusedDocumentId;
}
}
if (resolvedAnchor && isDocumentRowKey(resolvedAnchor)) {
return getRowId(resolvedAnchor) || null;
}
const lastDocKey = [...unique].reverse().find(isDocumentRowKey);
return lastDocKey ? getRowId(lastDocKey) || null : null;
})();
setFocusedDocumentId(nextFocusedDocumentId);
if (resolvedAnchor) {
selectionAnchorRef.current = resolvedAnchor;
} else if (!unique.length) {
selectionAnchorRef.current = null;
} else if (!selectionAnchorRef.current || !unique.includes(selectionAnchorRef.current)) {
selectionAnchorRef.current = unique[unique.length - 1];
}
return { selection: unique, focusKey: selectionAnchorRef.current };
},
[visibleRowKeySet, focusedDocumentId, updateSelectionOrder],
);
const promoteSelectionOrder = useCallback(
(docId) => {
if (!docId) return;
const rowKey = resolveDocumentRowKey(docId);
if (!rowKey) return;
if (!selectedRowKeys.includes(rowKey)) return;
updateSelectionOrder(selectedRowKeys, [rowKey]);
selectionAnchorRef.current = rowKey;
setFocusedDocumentId(docId);
setActivePreviewId(docId);
},
[selectedRowKeys, updateSelectionOrder],
);
const folderOptions = useMemo(() => {
const cache = new Map();
@@ -967,85 +894,38 @@ const AppLayout = () => {
[navigableRows],
);
useEffect(() => {
configureSelectionEnvironment({
visibleRowKeySet,
navigableRowKeys,
});
}, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]);
const handleRowSelection = useCallback(
(rowKey, event) => {
if (!rowKey || !visibleRowKeySet.has(rowKey)) {
return;
}
setFocusedRowKey(rowKey);
const shiftKey = Boolean(event?.shiftKey);
const metaKey = Boolean(event?.metaKey);
const ctrlKey = Boolean(event?.ctrlKey);
const additive = metaKey || ctrlKey;
if (shiftKey) {
event?.preventDefault?.();
}
let anchorKey = selectionAnchorRef.current;
if (!anchorKey && shiftKey && selectedRowKeys.length) {
anchorKey = selectedRowKeys[selectedRowKeys.length - 1];
}
if (!anchorKey) {
anchorKey = rowKey;
}
let nextKeys = [];
let interactedKeys = [];
if (shiftKey && anchorKey) {
const anchorIndex = navigableRowKeys.indexOf(anchorKey);
const targetIndex = navigableRowKeys.indexOf(rowKey);
if (anchorIndex !== -1 && targetIndex !== -1) {
const [start, end] =
anchorIndex <= targetIndex
? [anchorIndex, targetIndex]
: [targetIndex, anchorIndex];
const range = navigableRowKeys.slice(start, end + 1);
nextKeys = range;
const previousSet = new Set(selectedRowKeys);
interactedKeys = range.filter(
(key) => key === rowKey || !previousSet.has(key),
);
if (!interactedKeys.includes(rowKey)) {
interactedKeys.push(rowKey);
}
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
}
} else if (additive) {
if (selectedRowKeys.includes(rowKey)) {
nextKeys = selectedRowKeys.filter((key) => key !== rowKey);
interactedKeys = [];
} else {
nextKeys = [...selectedRowKeys, rowKey];
interactedKeys = [rowKey];
}
anchorKey = rowKey;
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
anchorKey = rowKey;
}
applySelection(nextKeys, { anchor: anchorKey, interactedKeys });
handleRowSelectionInternal(rowKey, event);
},
[
applySelection,
navigableRowKeys,
selectedRowKeys,
setFocusedRowKey,
visibleRowKeySet,
],
[handleRowSelectionInternal],
);
const openDetailPanel = useCallback(() => {
setDetailPanelOpen(true);
}, []);
const promoteSelectionOrder = useCallback(
(docId) => {
if (!docId) return;
promoteSelectionOrderInternal(docId);
const rowKey = resolveDocumentRowKey(docId);
if (rowKey) {
selectionAnchorRef.current = rowKey;
}
setFocusedDocumentId(docId);
setActivePreviewId(docId);
},
[
promoteSelectionOrderInternal,
selectionAnchorRef,
setFocusedDocumentId,
setActivePreviewId,
],
);
const handleDocumentRowClick = useCallback(
(documentId, event) => {
@@ -1054,16 +934,15 @@ const AppLayout = () => {
const hadSelection = selectionCount > 0;
handleRowSelection(rowKey, event);
if (!hadSelection) {
openDetailPanel();
detailPanelControlRef.current.open();
}
},
[handleRowSelection, openDetailPanel, selectionCount],
[handleRowSelection, selectionCount],
);
const clearDocumentSelection = useCallback(() => {
setFocusedRowKey(null);
applySelection([], { anchor: null, interactedKeys: [] });
}, [applySelection]);
clearSelectionInternal();
}, [clearSelectionInternal]);
const handleFolderRowClick = useCallback(
(folderId, event) => {
@@ -1086,7 +965,7 @@ const AppLayout = () => {
} else {
setFocusedRowKey((current) => (isFolderRowKey(current) ? current : null));
}
}, [focusedDocumentId]);
}, [focusedDocumentId, setFocusedRowKey]);
useEffect(() => {
if (!focusedRowKey) {
@@ -1105,7 +984,7 @@ const AppLayout = () => {
} else {
setFocusedRowKey(null);
}
}, [focusedRowKey, navigableRowKeys, focusedDocumentId]);
}, [focusedRowKey, navigableRowKeys, focusedDocumentId, setFocusedRowKey]);
const ensureFolderData = useCallback(
@@ -3203,6 +3082,8 @@ const AppLayout = () => {
setSelectionOrder,
setSelectedRowKeys,
setStatusMessage,
selectionAnchorRef,
selectionOrderRef,
],
);
@@ -3281,7 +3162,7 @@ const AppLayout = () => {
const openDocumentPreview = useCallback(
(documentId, { replace = false } = {}) => {
if (!documentId) return;
setDetailPanelOpen(false);
detailPanelControlRef.current.close();
previewReturnPathRef.current = `${location.pathname}${location.search}`;
navigate(`/documents/${documentId}`, { replace });
},
@@ -3338,6 +3219,7 @@ const AppLayout = () => {
selectedRowKeys,
navigableRows,
applySelection,
setFocusedRowKey,
]);
const handleDocumentListKeyDown = useCallback(
@@ -3386,7 +3268,7 @@ const AppLayout = () => {
selectFolder(row.id);
} else if (row.type === 'document') {
if (selectionCount === 0) {
openDetailPanel();
detailPanelControlRef.current.open();
}
openDocumentPreview(row.id);
}
@@ -3426,7 +3308,7 @@ const AppLayout = () => {
preventDefault: () => {},
});
if (targetRow.type === 'document' && !hadSelection) {
openDetailPanel();
detailPanelControlRef.current.open();
}
},
[
@@ -3435,10 +3317,10 @@ const AppLayout = () => {
focusedRowKey,
selectedRowKeys,
handleRowSelection,
openDetailPanel,
selectFolder,
openDocumentPreview,
selectionCount,
setFocusedRowKey,
],
);
@@ -4036,6 +3918,11 @@ const AppLayout = () => {
selectedFolder,
notifyApiError,
assetManager,
selectionOrderRef,
selectionAnchorRef,
setSelectedRowKeys,
setSelectionOrder,
setFocusedDocumentId,
]);
useEffect(() => {
@@ -4486,94 +4373,25 @@ const AppLayout = () => {
return ordered;
}, [selectionOrder, documentLookup, selectedDocumentIds]);
useEffect(() => {
if (!orderedSelectedDocuments.length) {
return;
}
const {
detailPanelOpen,
detailPanelSelectedDocuments,
openDetailPanel,
closeDetailPanel,
} = useDetailPanel({
selectedDocumentIds,
documentLookup,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
});
const nextIds = orderedSelectedDocuments
.map((doc) => (doc?.id ? doc.id : null))
.filter(Boolean);
setDetailPanelDocIds((prev) => {
if (nextIds.length === prev.length && nextIds.every((id, index) => id === prev[index])) {
return prev;
}
return nextIds;
});
}, [orderedSelectedDocuments]);
useEffect(() => {
if (!detailPanelDocIds.length) {
setDetailPanelDocs([]);
return;
}
setDetailPanelDocs((prevDocs) => {
const prevMap = new Map((prevDocs || []).map((doc) => [doc.id, doc]));
const next = detailPanelDocIds
.map((id) => documentLookup.get(id) || prevMap.get(id) || null)
.filter(Boolean);
if (next.length === prevDocs.length && next.every((doc, index) => doc === prevDocs[index])) {
return prevDocs;
}
return next;
});
}, [detailPanelDocIds, documentLookup]);
const detailPanelSelectedDocuments = detailPanelDocs;
const lastScrolledDetailDocRef = useRef(null);
useEffect(() => {
if (!detailPanelOpen) {
lastScrolledDetailDocRef.current = null;
return;
}
if (!orderedSelectedDocuments.length) {
lastScrolledDetailDocRef.current = null;
return;
}
let lastSelectedId = null;
for (let index = selectionOrder.length - 1; index >= 0; index -= 1) {
const key = selectionOrder[index];
if (isDocumentRowKey(key)) {
lastSelectedId = getRowId(key);
if (lastSelectedId) {
break;
}
}
}
if (!lastSelectedId && orderedSelectedDocuments.length) {
const fallbackDoc = orderedSelectedDocuments[orderedSelectedDocuments.length - 1];
lastSelectedId = fallbackDoc?.id || null;
}
if (!lastSelectedId || lastScrolledDetailDocRef.current === lastSelectedId) {
return;
}
if (typeof document === 'undefined') {
return;
}
const targetElement =
document.getElementById(`document-row-${lastSelectedId}`) ||
document.getElementById(`document-card-${lastSelectedId}`);
if (!targetElement) {
return;
}
lastScrolledDetailDocRef.current = lastSelectedId;
requestAnimationFrame(() => {
targetElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
});
}, [orderedSelectedDocuments, selectionOrder, documentsViewMode, detailPanelOpen]);
detailPanelControlRef.current = {
open: openDetailPanel,
close: closeDetailPanel,
};
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
const chain = [];
@@ -5035,8 +4853,8 @@ const AppLayout = () => {
);
const handleDetailPanelClose = useCallback(() => {
setDetailPanelOpen(false);
}, []);
closeDetailPanel();
}, [closeDetailPanel]);
const detailPanelProps = useMemo(
() => ({
+128
View File
@@ -0,0 +1,128 @@
import { useCallback, useEffect, useRef, useState } from 'react';
export const useDetailPanel = ({
documentLookup,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
}) => {
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
const lastScrolledDetailDocRef = useRef(null);
useEffect(() => {
if (!orderedSelectedDocuments.length) {
setDetailPanelDocIds((prev) => (prev.length ? [] : prev));
return;
}
const nextIds = orderedSelectedDocuments
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number');
setDetailPanelDocIds((prev) => {
if (prev.length === nextIds.length && prev.every((id, index) => id === nextIds[index])) {
return prev;
}
return nextIds;
});
}, [orderedSelectedDocuments]);
useEffect(() => {
if (!detailPanelDocIds.length) {
setDetailPanelDocs((prev) => (prev.length ? [] : prev));
return;
}
setDetailPanelDocs((prevDocs) => {
const prevMap = new Map((prevDocs || []).map((doc) => [doc.id, doc]));
const next = detailPanelDocIds
.map((id) => documentLookup.get(id) || prevMap.get(id) || null)
.filter(Boolean);
if (next.length === prevDocs.length && next.every((doc, index) => doc === prevDocs[index])) {
return prevDocs;
}
return next;
});
}, [detailPanelDocIds, documentLookup]);
useEffect(() => {
if (!detailPanelOpen) {
lastScrolledDetailDocRef.current = null;
return;
}
if (!orderedSelectedDocuments.length) {
lastScrolledDetailDocRef.current = null;
return;
}
let lastSelectedId = null;
for (let index = selectionOrder.length - 1; index >= 0; index -= 1) {
const key = selectionOrder[index];
if (isDocumentRowKey(key)) {
lastSelectedId = getRowId(key);
if (lastSelectedId) {
break;
}
}
}
if (!lastSelectedId && orderedSelectedDocuments.length) {
const fallbackDoc = orderedSelectedDocuments[orderedSelectedDocuments.length - 1];
lastSelectedId = fallbackDoc?.id || null;
}
if (!lastSelectedId || lastScrolledDetailDocRef.current === lastSelectedId) {
return;
}
if (typeof document === 'undefined') {
return;
}
const targetElement =
document.getElementById(`document-row-${lastSelectedId}`)
|| document.getElementById(`document-card-${lastSelectedId}`);
if (!targetElement) {
return;
}
lastScrolledDetailDocRef.current = lastSelectedId;
requestAnimationFrame(() => {
targetElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
});
}, [
detailPanelOpen,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
]);
const detailPanelSelectedDocuments = detailPanelDocs;
const openDetailPanel = useCallback(() => {
setDetailPanelOpen(true);
}, []);
const closeDetailPanel = useCallback(() => {
setDetailPanelOpen(false);
}, []);
return {
detailPanelOpen,
detailPanelSelectedDocuments,
openDetailPanel,
closeDetailPanel,
setDetailPanelOpen,
};
};
export default useDetailPanel;
+250
View File
@@ -0,0 +1,250 @@
import { useCallback, useRef, useState } from 'react';
const DEFAULT_INITIAL_SELECTION = [];
export const useDocumentSelection = ({
resolveDocumentRowKey,
resolveFolderRowKey,
isDocumentRowKey,
isFolderRowKey,
getRowId,
initialSelection = DEFAULT_INITIAL_SELECTION,
}) => {
const [selectedRowKeys, setSelectedRowKeys] = useState(initialSelection);
const [selectionOrder, setSelectionOrder] = useState(initialSelection);
const selectionOrderRef = useRef(initialSelection);
const selectionAnchorRef = useRef(null);
const selectionInitializedRef = useRef(false);
const [focusedDocumentId, setFocusedDocumentId] = useState(null);
const [focusedRowKey, setFocusedRowKey] = useState(null);
const visibleRowKeySetRef = useRef(new Set());
const navigableRowKeysRef = useRef([]);
const configureSelectionEnvironment = useCallback(({
visibleRowKeySet,
navigableRowKeys,
}) => {
if (visibleRowKeySet) {
visibleRowKeySetRef.current = visibleRowKeySet;
}
if (Array.isArray(navigableRowKeys)) {
navigableRowKeysRef.current = navigableRowKeys;
}
}, []);
const updateSelectionOrder = useCallback((nextSelection, interactedKeys = []) => {
const nextSet = new Set(nextSelection);
const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id));
const interacted = (interactedKeys || []).filter((id, index, array) => array.indexOf(id) === index);
const base = previousOrder.filter((id) => !interacted.includes(id));
const result = [...base];
interacted.forEach((id) => {
if (nextSet.has(id) && !result.includes(id)) {
result.push(id);
}
});
nextSelection.forEach((id) => {
if (!result.includes(id)) {
result.push(id);
}
});
if (
result.length !== selectionOrderRef.current.length
|| result.some((id, index) => selectionOrderRef.current[index] !== id)
) {
selectionOrderRef.current = result;
setSelectionOrder(result);
} else {
selectionOrderRef.current = result;
}
}, []);
const applySelection = useCallback(
(rowKeys, { anchor, interactedKeys = [] } = {}) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const unique = [];
(rowKeys || []).forEach((key) => {
if (!key) return;
let canonicalKey = null;
if (visibleRowKeySet.has(key)) {
canonicalKey = key;
} else if (isDocumentRowKey(key)) {
const id = getRowId(key);
canonicalKey = id ? resolveDocumentRowKey(id) : null;
} else if (isFolderRowKey(key)) {
const id = getRowId(key);
canonicalKey = id ? resolveFolderRowKey(id) : null;
}
if (!canonicalKey || !visibleRowKeySet.has(canonicalKey)) {
return;
}
if (!unique.includes(canonicalKey)) {
unique.push(canonicalKey);
}
});
let resolvedAnchor = anchor;
if (resolvedAnchor && !unique.includes(resolvedAnchor)) {
resolvedAnchor = null;
}
setSelectedRowKeys(unique);
updateSelectionOrder(unique, interactedKeys);
const nextFocusedDocumentId = (() => {
if (focusedDocumentId) {
const focusKey = resolveDocumentRowKey(focusedDocumentId);
if (focusKey && unique.includes(focusKey)) {
return focusedDocumentId;
}
}
if (resolvedAnchor && isDocumentRowKey(resolvedAnchor)) {
return getRowId(resolvedAnchor) || null;
}
const lastDocKey = [...unique].reverse().find(isDocumentRowKey);
return lastDocKey ? getRowId(lastDocKey) || null : null;
})();
setFocusedDocumentId(nextFocusedDocumentId);
if (resolvedAnchor) {
selectionAnchorRef.current = resolvedAnchor;
} else if (!unique.length) {
selectionAnchorRef.current = null;
} else if (!selectionAnchorRef.current || !unique.includes(selectionAnchorRef.current)) {
selectionAnchorRef.current = unique[unique.length - 1];
}
return { selection: unique, focusKey: selectionAnchorRef.current };
},
[
focusedDocumentId,
getRowId,
isDocumentRowKey,
isFolderRowKey,
resolveDocumentRowKey,
resolveFolderRowKey,
updateSelectionOrder,
],
);
const clearSelection = useCallback(() => {
setFocusedRowKey(null);
applySelection([], { anchor: null, interactedKeys: [] });
}, [applySelection]);
const handleRowSelection = useCallback(
(rowKey, event) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const navigableRowKeys = navigableRowKeysRef.current;
if (!rowKey || !visibleRowKeySet.has(rowKey)) {
return;
}
setFocusedRowKey(rowKey);
const shiftKey = Boolean(event?.shiftKey);
const metaKey = Boolean(event?.metaKey);
const ctrlKey = Boolean(event?.ctrlKey);
const additive = metaKey || ctrlKey;
if (shiftKey) {
event?.preventDefault?.();
}
let anchorKey = selectionAnchorRef.current;
if (!anchorKey && shiftKey && selectedRowKeys.length) {
anchorKey = selectedRowKeys[selectedRowKeys.length - 1];
}
if (!anchorKey) {
anchorKey = rowKey;
}
let nextKeys = [];
let interactedKeys = [];
if (shiftKey && anchorKey) {
const anchorIndex = navigableRowKeys.indexOf(anchorKey);
const targetIndex = navigableRowKeys.indexOf(rowKey);
if (anchorIndex !== -1 && targetIndex !== -1) {
const [start, end] = anchorIndex <= targetIndex
? [anchorIndex, targetIndex]
: [targetIndex, anchorIndex];
const range = navigableRowKeys.slice(start, end + 1);
nextKeys = range;
const previousSet = new Set(selectedRowKeys);
interactedKeys = range.filter((key) => key === rowKey || !previousSet.has(key));
if (!interactedKeys.includes(rowKey)) {
interactedKeys.push(rowKey);
}
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
}
} else if (additive) {
if (selectedRowKeys.includes(rowKey)) {
nextKeys = selectedRowKeys.filter((key) => key !== rowKey);
interactedKeys = [];
} else {
nextKeys = [...selectedRowKeys, rowKey];
interactedKeys = [rowKey];
}
anchorKey = rowKey;
} else {
nextKeys = [rowKey];
interactedKeys = [rowKey];
anchorKey = rowKey;
}
applySelection(nextKeys, { anchor: anchorKey, interactedKeys });
},
[applySelection, selectedRowKeys],
);
const promoteSelectionOrder = useCallback(
(docId) => {
if (!docId) return;
const rowKey = resolveDocumentRowKey(docId);
if (!rowKey) return;
if (!selectedRowKeys.includes(rowKey)) {
return;
}
updateSelectionOrder(selectedRowKeys, [rowKey]);
},
[resolveDocumentRowKey, selectedRowKeys, updateSelectionOrder],
);
return {
selectedRowKeys,
setSelectedRowKeys,
selectionOrder,
setSelectionOrder,
selectionOrderRef,
selectionAnchorRef,
selectionInitializedRef,
focusedDocumentId,
setFocusedDocumentId,
focusedRowKey,
setFocusedRowKey,
applySelection,
clearSelection,
handleRowSelection,
promoteSelectionOrder,
configureSelectionEnvironment,
};
};
export default useDocumentSelection;