@pdftron/webviewer-react-toolkit
Version:
A React component library for integrating with PDFTron WebViewer API.
156 lines (155 loc) • 6.18 kB
JavaScript
import { useCallback, useEffect, useMemo, useState } from 'react';
import { moveMultiFromIndexToIndex } from '../utils';
/**
* Combines most of the necessary functionality to manage files for the
* `FileOrganizer` component.
* @param options Options for managing files.
*/
export function useManagedFiles({ initialFiles, preventMultiDrag, preventDeselectOnDragOther, preventSelectOnDrag, preventMultiSelect, easyMultiSelect, } = {}) {
const [files, setFiles] = useState(initialFiles ?? []);
const addFile = useCallback((file, index) => {
setFiles((prev) => {
if (prev.includes(file))
return prev;
if (index === undefined)
return [...prev, file];
return [...prev.slice(0, index), file, ...prev.slice(index)];
});
}, []);
const removeFile = useCallback((file) => setFiles((prev) => prev.filter((f) => f !== file)), []);
/* --- Selection. --- */
// Note: this is also an ordered stack, with the most recently selected last.
// This allows us to do shift-range selection from the most recent.
const [selectedIds, setSelectedIds] = useState([]);
const toggleSelectedId = useCallback((id, event) => {
const multiKey = !preventMultiSelect && (event?.ctrlKey || event?.metaKey || false);
const rangeKey = !preventMultiSelect && (event?.shiftKey || false);
setSelectedIds((prev) => {
const toggleIndex = prev.indexOf(id);
if (!multiKey && rangeKey && prev.length) {
const lastSelectedIndex = files.findIndex((f) => f.id === prev[prev.length - 1]);
const toggleFileIndex = files.findIndex((f) => f.id === id);
let selectedFiles = [];
if (toggleFileIndex < lastSelectedIndex) {
selectedFiles = files.slice(toggleFileIndex, lastSelectedIndex + 1);
}
else {
selectedFiles = files.slice(lastSelectedIndex, toggleFileIndex + 1).reverse();
}
return selectedFiles.map((f) => f.id);
}
if (toggleIndex === -1) {
if (easyMultiSelect || multiKey)
return [...prev, id];
return [id];
}
if (toggleIndex !== -1) {
if (easyMultiSelect || multiKey)
return [...prev.slice(0, toggleIndex), ...prev.slice(toggleIndex + 1)];
if (prev.length > 1)
return [id];
return [];
}
return prev;
});
}, [easyMultiSelect, files, preventMultiSelect]);
const onDeselectAll = useCallback(() => setSelectedIds([]), []);
const onSelectAll = useCallback(() => setSelectedIds(files.map((file) => file.id)), [files]);
const _setMovingSelectedId = useCallback((id) => {
setSelectedIds((prev) => {
if (prev.includes(id))
return prev;
if (preventDeselectOnDragOther)
return preventSelectOnDrag ? prev : [...prev, id];
return preventSelectOnDrag ? [] : [id];
});
}, [preventDeselectOnDragOther, preventSelectOnDrag]);
/* --- Multiple drag items. --- */
const [draggingIds, setDraggingIds] = useState([]);
const onDragChange = useCallback((id) => {
if (!id)
return setDraggingIds([]);
if (selectedIds.length === 0)
return;
_setMovingSelectedId(id);
if (!preventMultiDrag) {
const toDragIds = selectedIds.includes(id) ? selectedIds : [id];
setDraggingIds(toDragIds);
}
else {
setDraggingIds([id]);
}
}, [selectedIds, _setMovingSelectedId, preventMultiDrag]);
/* --- Moving items. --- */
const onMove = useCallback((fromIndex, toIndex) => {
const fromFile = files[fromIndex];
if (!fromFile)
return false;
// Update selections.
_setMovingSelectedId(fromFile.id);
// If multi drag is permitted, and multiple items are selected, and
// there are no items being dragged, do a multi move. This will be a
// keyboard-specific operation, as multi dragging is managed by the
// dragging handlers.
if (!preventMultiDrag && selectedIds.includes(fromFile.id) && selectedIds.length > 1) {
const next = moveMultiFromIndexToIndex(files, selectedIds, fromIndex, toIndex);
if (next === files)
return false;
setFiles(next);
return true;
}
// Don't allow "wrapping".
if (toIndex < 0 || toIndex >= files.length)
return false;
const clone = files.slice();
const item = clone.splice(fromIndex, 1)[0];
clone.splice(toIndex, 0, item);
setFiles(clone);
return true;
}, [_setMovingSelectedId, files, preventMultiDrag, selectedIds]);
// Remove selected items if the file is removed.
useEffect(() => {
setSelectedIds((prev) => {
const toRemove = new Set(prev);
files.forEach((file) => {
if (toRemove.has(file.id))
toRemove.delete(file.id);
});
return prev.filter((id) => !toRemove.has(id));
});
}, [files]);
const managedFiles = useMemo(() => ({
fileOrganizerProps: {
files,
onMove,
onDragChange,
onDeselectAll,
onSelectAll,
draggingIds: draggingIds,
},
getThumbnailSelectionProps: (id) => ({
selected: selectedIds.includes(id),
onClick: (event) => toggleSelectedId(id, event),
}),
files,
setFiles,
selectedIds,
setSelectedIds,
toggleSelectedId,
addFile,
removeFile,
draggingIds,
}), [
addFile,
draggingIds,
files,
onMove,
onDragChange,
removeFile,
onSelectAll,
selectedIds,
toggleSelectedId,
onDeselectAll,
]);
return managedFiles;
}