UNPKG

chonky

Version:

A File Browser component for React

304 lines (297 loc) 12.6 kB
import { reduxActions } from '../redux/reducers'; import { getFileData, getIsFileSelected, selectDisableSelection, selectors, selectParentFolder, selectSelectionSize } from '../redux/selectors'; import { reduxThunks } from '../redux/thunks'; import { thunkRequestFileAction } from '../redux/thunks/dispatchers.thunks'; import { ChangeSelectionPayload, EndDragNDropPayload, KeyboardClickFilePayload, MouseClickFilePayload, MoveFilesPayload, OpenFileContextMenuPayload, OpenFilesPayload, StartDragNDropPayload } from '../types/action-payloads.types'; import { ChonkyIconName } from '../types/icons.types'; import { FileHelper } from '../util/file-helper'; import { defineFileAction } from '../util/helpers'; import { Logger } from '../util/logger'; import { ChonkyActions } from './index'; export const EssentialActions = { /** * Action that is dispatched when the user clicks on a file entry using their mouse. * Both single clicks and double clicks trigger this action. */ MouseClickFile: defineFileAction( { id: 'mouse_click_file', __payloadType: {} as MouseClickFilePayload, } as const, ({ payload, reduxDispatch, getReduxState }) => { if (payload.clickType === 'double') { if (FileHelper.isOpenable(payload.file)) { reduxDispatch( thunkRequestFileAction(ChonkyActions.OpenFiles, { targetFile: payload.file, // To simulate Windows Explorer and Nautilus behaviour, // a double click on a file only opens that file even if // there is a selection. files: [payload.file], }) ); } } else { // We're dealing with a single click const disableSelection = selectDisableSelection(getReduxState()); if (FileHelper.isSelectable(payload.file) && !disableSelection) { if (payload.ctrlKey) { // Multiple selection reduxDispatch( reduxActions.toggleSelection({ fileId: payload.file.id, exclusive: false, }) ); reduxDispatch( reduxActions.setLastClickIndex({ index: payload.fileDisplayIndex, fileId: payload.file.id, }) ); } else if (payload.shiftKey) { // Range selection const lastClickIndex = selectors.getLastClickIndex(getReduxState()); if (typeof lastClickIndex === 'number') { // We have the index of the previous click let rangeStart = lastClickIndex; let rangeEnd = payload.fileDisplayIndex; if (rangeStart > rangeEnd) { [rangeStart, rangeEnd] = [rangeEnd, rangeStart]; } reduxDispatch(reduxThunks.selectRange({ rangeStart, rangeEnd })); } else { // Since we can't do a range selection, do a // multiple selection reduxDispatch( reduxActions.toggleSelection({ fileId: payload.file.id, exclusive: false, }) ); reduxDispatch( reduxActions.setLastClickIndex({ index: payload.fileDisplayIndex, fileId: payload.file.id, }) ); } } else { // Exclusive selection reduxDispatch( reduxActions.toggleSelection({ fileId: payload.file.id, exclusive: true, }) ); reduxDispatch( reduxActions.setLastClickIndex({ index: payload.fileDisplayIndex, fileId: payload.file.id, }) ); } } else { if (!payload.ctrlKey && !disableSelection) { reduxDispatch(reduxActions.clearSelection()); } reduxDispatch( reduxActions.setLastClickIndex({ index: payload.fileDisplayIndex, fileId: payload.file.id, }) ); } } } ), /** * Action that is dispatched when the user "clicks" on a file using their keyboard. * Using Space and Enter keys counts as clicking. */ KeyboardClickFile: defineFileAction( { id: 'keyboard_click_file', __payloadType: {} as KeyboardClickFilePayload, } as const, ({ payload, reduxDispatch, getReduxState }) => { reduxDispatch( reduxActions.setLastClickIndex({ index: payload.fileDisplayIndex, fileId: payload.file.id, }) ); if (payload.enterKey) { // We only dispatch the Open Files action here when the selection is // empty. Otherwise, `Enter` key presses are handled by the // hotkey manager for the Open Files action. if (selectSelectionSize(getReduxState()) === 0) { reduxDispatch( thunkRequestFileAction(ChonkyActions.OpenFiles, { targetFile: payload.file, files: [payload.file], }) ); } } else if (payload.spaceKey && FileHelper.isSelectable(payload.file)) { reduxDispatch( reduxActions.toggleSelection({ fileId: payload.file.id, exclusive: payload.ctrlKey, }) ); } } ), /** * Action that is dispatched when user starts dragging some file. */ StartDragNDrop: defineFileAction( { id: 'start_drag_n_drop', __payloadType: {} as StartDragNDropPayload, } as const, ({ payload, reduxDispatch, getReduxState }) => { const file = payload.draggedFile; if (!getIsFileSelected(getReduxState(), file)) { if (FileHelper.isSelectable(file)) { reduxDispatch( reduxActions.selectFiles({ fileIds: [file.id], reset: true, }) ); } } } ), /** * Action that is dispatched when user either cancels the drag & drop interaction, * or drops a file somewhere. */ EndDragNDrop: defineFileAction( { id: 'end_drag_n_drop', __payloadType: {} as EndDragNDropPayload, } as const, ({ payload, reduxDispatch, getReduxState }) => { if (getIsFileSelected(getReduxState(), payload.destination)) { // Can't drop a selection into itself return; } const { draggedFile, selectedFiles } = payload as EndDragNDropPayload; const droppedFiles = selectedFiles.length > 0 ? selectedFiles : [draggedFile]; reduxDispatch( thunkRequestFileAction(ChonkyActions.MoveFiles, { ...payload, files: droppedFiles, }) ); } ), /** * Action that is dispatched when user moves files from one folder to another, * usually by dragging & dropping some files into the folder. */ MoveFiles: defineFileAction({ id: 'move_files', __payloadType: {} as MoveFilesPayload, } as const), /** * Action that is dispatched when the selection changes for any reason. */ ChangeSelection: defineFileAction({ id: 'change_selection', __payloadType: {} as ChangeSelectionPayload, } as const), /** * Action that is dispatched when user wants to open some files. This action is * often triggered by other actions. */ OpenFiles: defineFileAction({ id: 'open_files', __payloadType: {} as OpenFilesPayload, } as const), /** * Action that is triggered when user wants to go up a directory. */ OpenParentFolder: defineFileAction( { id: 'open_parent_folder', hotkeys: ['backspace'], button: { name: 'Go up a directory', toolbar: true, contextMenu: false, icon: ChonkyIconName.openParentFolder, iconOnly: true, }, } as const, ({ reduxDispatch, getReduxState }) => { const parentFolder = selectParentFolder(getReduxState()); if (FileHelper.isOpenable(parentFolder)) { reduxDispatch( thunkRequestFileAction(ChonkyActions.OpenFiles, { targetFile: parentFolder, files: [parentFolder], }) ); } else { Logger.warn( 'Open parent folder effect was triggered even though the parent folder' + ' is not openable. This indicates a bug in presentation components.' ); } } ), /** * Action that is dispatched when user opens the context menu, either by right click * on something or using the context menu button on their keyboard. */ OpenFileContextMenu: defineFileAction( { id: 'open_file_context_menu', __payloadType: {} as OpenFileContextMenuPayload, } as const, ({ payload, reduxDispatch, getReduxState }) => { // TODO: Check if the context menu component is actually enabled. There is a // chance it doesn't matter if it is enabled or not - if it is not mounted, // the action will simply have no effect. It also allows users to provide // their own components - however, users could also flip the "context menu // component mounted" switch... const triggerFile = getFileData(getReduxState(), payload.triggerFileId); if (triggerFile) { const fileSelected = getIsFileSelected(getReduxState(), triggerFile); if (!fileSelected) { // If file is selected, we leave the selection as is. If it is not // selected, it means user right clicked the file with no selection. // We simulate the Windows Explorer/Nautilus behaviour of moving // selection to this file. if (FileHelper.isSelectable(triggerFile)) { reduxDispatch( reduxActions.selectFiles({ fileIds: [payload.triggerFileId], reset: true, }) ); } else { reduxDispatch(reduxActions.clearSelection()); } } } reduxDispatch( reduxActions.showContextMenu({ triggerFileId: payload.triggerFileId, mouseX: payload.clientX - 2, mouseY: payload.clientY - 4, }) ); } ), };