chonky
Version:
A File Browser component for React
256 lines (229 loc) • 11.1 kB
text/typescript
import sort from 'fast-sort';
import FuzzySearch from 'fuzzy-search';
import { Nilable, Nullable } from 'tsdef';
import { createSelector } from '@reduxjs/toolkit';
import { OptionIds } from '../action-definitions/option-ids';
import { FileArray, FileData, FileFilter } from '../types/file.types';
import { RootState } from '../types/redux.types';
import { FileSortKeySelector, SortOrder } from '../types/sort.types';
import { FileHelper } from '../util/file-helper';
// Raw selectors
export const selectInstanceId = (state: RootState) => state.instanceId;
export const selectExternalFileActionHandler = (state: RootState) => state.externalFileActionHandler;
export const selectFileActionMap = (state: RootState) => state.fileActionMap;
export const selectFileActionIds = (state: RootState) => state.fileActionIds;
export const selectFileActionData = (fileActionId: string) => (state: RootState) =>
selectFileActionMap(state)[fileActionId];
export const selectToolbarItems = (state: RootState) => state.toolbarItems;
export const selectContextMenuItems = (state: RootState) => state.contextMenuItems;
export const selectFolderChain = (state: RootState) => state.folderChain;
export const selectCurrentFolder = (state: RootState) => {
const folderChain = selectFolderChain(state);
const currentFolder = folderChain.length > 0 ? folderChain[folderChain.length - 1] : null;
return currentFolder;
};
export const selectParentFolder = (state: RootState) => {
const folderChain = selectFolderChain(state);
const parentFolder = folderChain.length > 1 ? folderChain[folderChain.length - 2] : null;
return parentFolder;
};
export const selectRawFiles = (state: RootState) => state.rawFiles;
export const selectFileMap = (state: RootState) => state.fileMap;
export const selectCleanFileIds = (state: RootState) => state.cleanFileIds;
export const selectFileData = (fileId: Nullable<string>) => (state: RootState) =>
fileId ? selectFileMap(state)[fileId] : null;
export const selectHiddenFileIdMap = (state: RootState) => state.hiddenFileIdMap;
export const selectHiddenFileCount = (state: RootState) => Object.keys(selectHiddenFileIdMap(state)).length;
export const selectFocusSearchInput = (state: RootState) => state.focusSearchInput;
export const selectSearchString = (state: RootState) => state.searchString;
export const selectSelectionMap = (state: RootState) => state.selectionMap;
export const selectSelectedFileIds = (state: RootState) => Object.keys(selectSelectionMap(state));
export const selectSelectionSize = (state: RootState) => selectSelectedFileIds(state).length;
export const selectIsFileSelected = (fileId: Nullable<string>) => (state: RootState) =>
!!fileId && !!selectSelectionMap(state)[fileId];
export const selectSelectedFiles = (state: RootState) => {
const fileMap = selectFileMap(state);
return Object.keys(selectSelectionMap(state)).map(id => fileMap[id]);
};
export const selectSelectedFilesForAction = (fileActionId: string) => (state: RootState) => {
const { fileActionMap } = state;
const action = fileActionMap[fileActionId];
if (!action || !action.requiresSelection) return undefined;
return getSelectedFiles(state, action.fileFilter);
};
export const selectSelectedFilesForActionCount = (fileActionId: string) => (state: RootState) =>
getSelectedFilesForAction(state, fileActionId)?.length;
export const selectDisableSelection = (state: RootState) => state.disableSelection;
export const selectFileViewConfig = (state: RootState) => state.fileViewConfig;
export const selectSortActionId = (state: RootState) => state.sortActionId;
export const selectSortOrder = (state: RootState) => state.sortOrder;
export const selectOptionMap = (state: RootState) => state.optionMap;
export const selectOptionValue = (optionId: string) => (state: RootState) => selectOptionMap(state)[optionId];
export const selectThumbnailGenerator = (state: RootState) => state.thumbnailGenerator;
export const selectDoubleClickDelay = (state: RootState) => state.doubleClickDelay;
export const selectIsDnDDisabled = (state: RootState) => state.disableDragAndDrop;
export const selectClearSelectionOnOutsideClick = (state: RootState) => state.clearSelectionOnOutsideClick;
export const selectContextMenuMounted = (state: RootState) => state.contextMenuMounted;
export const selectContextMenuConfig = (state: RootState) => state.contextMenuConfig;
export const selectContextMenuTriggerFile = (state: RootState) => {
const config = selectContextMenuConfig(state);
if (!config || !config.triggerFileId) return null;
const fileMap = selectFileMap(state);
return fileMap[config.triggerFileId] ?? null;
};
// Raw selectors
const getFileActionMap = (state: RootState) => state.fileActionMap;
const getOptionMap = (state: RootState) => state.optionMap;
const getFileMap = (state: RootState) => state.fileMap;
const getFileIds = (state: RootState) => state.fileIds;
const getCleanFileIds = (state: RootState) => state.cleanFileIds;
const getSortActionId = (state: RootState) => state.sortActionId;
const getSortOrder = (state: RootState) => state.sortOrder;
const getSearchString = (state: RootState) => state.searchString;
const _getLastClick = (state: RootState) => state.lastClick;
// Memoized selectors
const makeGetAction = (fileActionSelector: (state: RootState) => Nullable<string>) =>
createSelector([getFileActionMap, fileActionSelector], (fileActionMap, fileActionId) =>
fileActionId && fileActionMap[fileActionId] ? fileActionMap[fileActionId] : null
);
const makeGetOptionValue = (optionId: string, defaultValue: any = undefined) =>
createSelector([getOptionMap], optionMap => {
const value = optionMap[optionId];
if (value === undefined) {
return defaultValue;
}
return value;
});
const makeGetFiles = (fileIdsSelector: (state: RootState) => Nullable<string>[]) =>
createSelector(
[getFileMap, fileIdsSelector],
(fileMap, fileIds): FileArray => fileIds.map(fileId => (fileId && fileMap[fileId] ? fileMap[fileId] : null))
);
const getSortedFileIds = createSelector(
[
getFileIds,
getSortOrder,
makeGetFiles(getFileIds),
makeGetAction(getSortActionId),
makeGetOptionValue(OptionIds.ShowFoldersFirst, false),
],
(fileIds, sortOrder, files, sortAction, showFolderFirst) => {
if (!sortAction) {
// We allow users to set the sort action ID to `null` if they want to use their
// own sorting mechanisms instead of relying on Chonky built-in sort.
return fileIds;
}
const prepareSortKeySelector = (selector: FileSortKeySelector) => (file: Nullable<FileData>) => selector(file);
const sortFunctions: {
asc?: (file: FileData) => any;
desc?: (file: FileData) => any;
}[] = [];
if (showFolderFirst) {
// If option is undefined (relevant actions is not enabled), we don't show
// folders first.
sortFunctions.push({
desc: prepareSortKeySelector(FileHelper.isDirectory),
});
}
if (sortAction.sortKeySelector) {
const configKeyName = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
sortFunctions.push({
[configKeyName]: prepareSortKeySelector(sortAction.sortKeySelector),
});
}
if (sortFunctions.length === 0) return fileIds;
// We copy the array because `fast-sort` mutates it
const sortedFileIds = sort([...files])
.by(sortFunctions as any)
.map(file => (file ? file.id : null));
return sortedFileIds;
}
);
const getSearcher = createSelector(
[makeGetFiles(getCleanFileIds)],
cleanFiles => new FuzzySearch(cleanFiles as FileData[], ['name'], { caseSensitive: false })
);
const getSearchFilteredFileIds = createSelector(
[getCleanFileIds, getSearchString, getSearcher],
(cleanFileIds, searchString, searcher) =>
searchString ? searcher.search(searchString).map(f => f.id) : cleanFileIds
);
const getHiddenFileIdMap = createSelector(
[getSearchFilteredFileIds, makeGetFiles(getCleanFileIds), makeGetOptionValue(OptionIds.ShowHiddenFiles)],
(searchFilteredFileIds, cleanFiles, showHiddenFiles) => {
const searchFilteredFileIdsSet = new Set(searchFilteredFileIds);
const hiddenFileIdMap: any = {};
cleanFiles.forEach(file => {
if (!file) return;
else if (!searchFilteredFileIdsSet.has(file.id)) {
// Hidden by seach
hiddenFileIdMap[file.id] = true;
} else if (!showHiddenFiles && FileHelper.isHidden(file)) {
// Hidden by options
hiddenFileIdMap[file.id] = true;
}
});
return hiddenFileIdMap;
}
);
const getDisplayFileIds = createSelector(
[getSortedFileIds, getHiddenFileIdMap],
/** Returns files that will actually be shown to the user. */
(sortedFileIds, hiddenFileIdMap) => sortedFileIds.filter(id => !id || !hiddenFileIdMap[id])
);
const getLastClickIndex = createSelector(
[_getLastClick, getSortedFileIds],
/** Returns the last click index after ensuring it is actually still valid. */
(lastClick, displayFileIds) => {
if (
!lastClick ||
lastClick.index > displayFileIds.length - 1 ||
lastClick.fileId != displayFileIds[lastClick.index]
) {
return null;
}
return lastClick.index;
}
);
export const selectors = {
// Raw selectors
getFileActionMap,
getOptionMap,
getFileMap,
getFileIds,
getCleanFileIds,
getSortActionId,
getSortOrder,
getSearchString,
_getLastClick,
// Memoized selectors
getSortedFileIds,
getSearcher,
getSearchFilteredFileIds,
getHiddenFileIdMap,
getDisplayFileIds,
getLastClickIndex,
// Parametrized selectors
makeGetAction,
makeGetOptionValue,
makeGetFiles,
};
// Selectors meant to be used outside of Redux code
export const getFileData = (state: RootState, fileId: Nullable<string>) =>
fileId ? selectFileMap(state)[fileId] : null;
export const getIsFileSelected = (state: RootState, file: FileData) => {
// !!! We deliberately don't use `FileHelper.isSelectable` here as we want to
// reflect the state of Redux store accurately.
return !!selectSelectionMap(state)[file.id];
};
export const getSelectedFiles = (state: RootState, ...filters: Nilable<FileFilter>[]) => {
const { fileMap, selectionMap } = state;
const selectedFiles = Object.keys(selectionMap).map(id => fileMap[id]);
const filteredSelectedFiles = filters.reduce(
(prevFiles, filter) => (filter ? prevFiles.filter(filter) : prevFiles),
selectedFiles
);
return filteredSelectedFiles;
};
export const getSelectedFilesForAction = (state: RootState, fileActionId: string) =>
selectSelectedFilesForAction(fileActionId)(state);