UNPKG

chonky

Version:

A File Browser component for React

256 lines (229 loc) 11.1 kB
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);