import autobind from "autobind-decorator" ; import { remote } from "electron" ; import { List } from "immutable" ; import path from "path" ; import * as PropTypes from "prop-types" ; import * as React from "react" ; import { HotKeys } from "react-hotkeys" ; const dialog = remote . dialog ; import { DirectoryItem , InputItem } from "components/blocks" ; import { Goto } from "components/modals" ; import DirectoryError from "errors/DirectoryError" ; import LoggedError from "errors/LoggedError" ; import { IDirectoryItem , IHandlers } from "models" ; import { DirectoryListModel } from "objects" ; import { IDirectoryListProps } from "props/panels" ; import { IDirectoryListState } from "states/panels" ; import { ClipboardAction , DirectoryDirection , ItemType , ScrollToDirection } from "types" ; import Utils from "Utils" ; import "styles/panels/DirectoryList.scss" ; /** The component for displaying a directory's list of items. */ class DirectoryList extends React . Component < IDirectoryListProps , IDirectoryListState > { /** Context available within a ScrollArea. */ public context : { scrollArea : any ; } ; /** Validation for context types. */ public static contextTypes = { scrollArea : PropTypes . object } ; /** Handler functions for the given events this component handles. */ private handlers : IHandlers = { chooseItem : this . toggleItemChosen , copy : ( ) => this . storeItemInClipboard ( "copy" ) , cut : ( ) => this . storeItemInClipboard ( "cut" ) , delete : this . delete , moveBack : this . goBack , moveDown : ( ) => this . move ( "down" ) , moveUp : ( ) => this . move ( "up" ) , newFile : ( ) => this . inputNewItem ( "file" ) , newFolder : ( ) => this . inputNewItem ( "folder" ) , openGoto : this . openGoto , paste : this . pasteFromClipboard , rename : this . inputRenameItem , scrollToBottom : ( ) => this . scrollTo ( "bottom" ) , scrollToTop : ( ) => this . scrollTo ( "top" ) , sendToTrash : this . sendToTrash , toggleShowHidden : this . toggleShowHidden } ; /** The internal model of this DirectoryList. */ private model : DirectoryListModel ; /**
     * A trapper that can be given focus in cases where focus on directory items
     * is lost.
     */ private keysTrapper ? : HotKeys | null ; /** Gets the keys trapper to invoke manual focusing of this DirectoryList. */ public get KeysTrapper ( ) : HotKeys | null | undefined { return this . keysTrapper ; } /** Gets the directory items that are not currently hidden. */ private get nonHiddenDirectoryItems ( ) : IDirectoryItem [ ] { return this . state . directoryItems . filter ( item => ! item . isHidden || this . state . showHiddenItems ) ; } /** Gets the currently selected item(s). */ private get selectedItems ( ) : IDirectoryItem [ ] { return this . state . chosenItems . length > 0 ? this . state . chosenItems : [ this . nonHiddenDirectoryItems [ this . state . selectedIndex ] ] ; } param props public constructor ( props , context ) { super ( props , context ) ; this . context = context ; this . state = { chosenItems : [ ] , creatingNewItem : false , directoryItems : [ ] , isFocused : this . props . isSelectedPane , isGotoOpen : false , itemDeleted : false , renamingItem : false , selectedIndex : 0 , showHiddenItems : false } ; this . model = new DirectoryListModel ( ) ; } /** Updates the directory contents after loading the component. */ public async componentDidMount ( ) { const { directoryManager , settingsManager } = this . props ; this . startWatcher ( ) ; const items = await directoryManager . listDirectory ( this . props . path , { hideUnixStyleHiddenItems : settingsManager . settings . windows . hideUnixStyleHiddenItems } ) ; this . setState ( { directoryItems : items } as IDirectoryListState ) ; } /** Handles closing the watcher on unmounting the directory list. */ public componentWillUnmount ( ) { this . props . directoryManager . stopWatching ( ) ; } param nextProps public componentWillReceiveProps ( nextProps ) { if ( ! this . props . isSelectedPane && nextProps . isSelectedPane ) { this . setState ( { isFocused : true } as IDirectoryListState ) ; } } param prevProps param prevState public async componentDidUpdate ( prevProps , prevState ) { const { directoryManager , settingsManager } = this . props ; this . props . statusNotifier . setItemCount ( this . nonHiddenDirectoryItems . length ) ; this . props . statusNotifier . setChosenCount ( this . state . chosenItems . length ) ; if ( this . nonHiddenDirectoryItems . length === 0 && this . keysTrapper && this . state . isFocused ) { Utils . autoFocus ( this . keysTrapper ) ; } if ( prevProps . path === this . props . path && ! prevState . creatingNewItem && ! prevState . renamingItem && ! this . state . itemDeleted ) { return ; } if ( prevState . itemDeleted ) { this . setState ( { itemDeleted : false } as IDirectoryListState ) ; } if ( prevProps . path !== this . props . path ) { this . startWatcher ( ) ; } const directoryItems = await directoryManager . listDirectory ( this . props . path , { hideUnixStyleHiddenItems : settingsManager . settings . windows . hideUnixStyleHiddenItems } ) ; const remainingChosenItems = this . state . chosenItems . filter ( item => directoryItems . includes ( item ) ) ; this . setState ( { chosenItems : remainingChosenItems , directoryItems } as IDirectoryListState ) ; } param nextProps param nextState public shouldComponentUpdate ( nextProps , nextState ) { return ( this . model . stateChanged ( this . state , nextState ) || this . model . propsChanged ( this . props , nextProps ) || this . model . chosenItemsChanged ( this . state . chosenItems , nextState . chosenItems ) || this . model . directoryItemsChanged ( this . state . directoryItems , nextState . directoryItems ) ) ; } returns public render ( ) { const items = this . renderItems ( ) ; return ( < div className = "DirectoryList" > < HotKeys handlers = { this . handlers } ref = { component => { this . keysTrapper = component ; } } onFocus = { this . setFocused } onBlur = { this . setUnFocused } > < ul onKeyDown = { this . handleKeyDown } > { items } { this . state . creatingNewItem && < InputItem creatingItemType = { this . state . creatingNewItem } sendUpCreateItem = { this . createNewItem } otherItems = { this . state . directoryItems } theme = { this . props . theme } / > } < /ul> < /HotKeys> < Goto initialPath = { this . props . path } isOpen = { this . state . isGotoOpen } onClose = { this . closeGoto } navigateTo = { this . navigateToPath } directoryManager = { this . props . directoryManager } settingsManager = { this . props . settingsManager } theme = { this . props . theme } / > < /div>); } private renderItems ( ) { return this . nonHiddenDirectoryItems . map ( ( item , index ) => { const isSelectedItem = this . props . isSelectedPane && ! this . state . creatingNewItem && this . state . selectedIndex === index ; if ( this . state . renamingItem && isSelectedItem ) { const thisItem = this . nonHiddenDirectoryItems . find ( i => i . name === item . name ) ; const otherItems = this . state . directoryItems . filter ( i => i . name !== item . name ) ; return < InputItem thisItem = { thisItem } otherItems = { otherItems } sendUpRenameItem = { this . renameItem } theme = { this . props . theme } / > ; } else { return < DirectoryItem key = { item . path } model = { item } isSelected = { this . state . isFocused && isSelectedItem } isChosen = { this . state . chosenItems . includes ( item ) } sendPathUp = { this . goIn } sendSelectedItemUp = { this . selectItem } sendDeletionUp = { this . refreshAfterDelete } theme = { this . props . theme } columnSizes = { this . props . columnSizes } / > ; } } ) ; } @ autobind private closeGoto ( ) { if ( this . state . isGotoOpen ) { this . setState ( { isGotoOpen : false } as IDirectoryListState ) ; } } @ autobind private async createNewItem ( itemName ? , itemTypeToCreate ? ) { if ( itemName && itemTypeToCreate ) { Utils . trace ( `Requesting to create ${ itemTypeToCreate } called ${ itemName } at ${ this . props . path }` ) ; await this . props . directoryManager . createItem ( itemName , this . props . path , itemTypeToCreate ) ; } this . setState ( { creatingNewItem : false } as IDirectoryListState ) ; } @ autobind private async delete ( ) { const chosenItems = this . selectedItems . length > 1 ? "the chosen items" : `'${ this . selectedItems [ 0 ] . name }'` ; const confirmDelete = this . props . settingsManager . settings . confirmation . requiredBeforeDeletion ? confirmationDialog ( `Are you sure you want to permanently delete ${ chosenItems }?` ) : true ; this . keysTrapper && Utils . autoFocus ( this . keysTrapper ) ; if ( confirmDelete ) { Utils . trace ( `Requesting to delete ${ this . selectedItems . map ( item => item . path ) . join ( ", " ) }` ) ; await this . props . directoryManager . deleteItems ( this . selectedItems ) ; this . refreshAfterDelete ( ) ; this . props . statusNotifier . notify ( "Deleted items" ) ; } } @ autobind private inputNewItem ( itemTypeToCreate ) { this . setState ( { creatingNewItem : itemTypeToCreate } as IDirectoryListState ) ; } @ autobind private inputRenameItem ( ) { this . setState ( { renamingItem : true } as IDirectoryListState ) ; } @ autobind private goBack ( ) { this . context . scrollArea . scrollTop ( ) ; const cachedSelectedIndex = this . model . popSelectedIndex ( ) ; const parentDirectory = path . join ( this . props . path , ".." ) ; this . setState ( { selectedIndex : cachedSelectedIndex || 0 } as IDirectoryListState ) ; this . props . sendPathUp ( parentDirectory ) ; } @ autobind private goIn ( pathToDirectory ) { this . context . scrollArea . scrollTop ( ) ; this . model . cacheSelectedIndex ( this . state . selectedIndex ) ; this . setState ( { selectedIndex : 0 } as IDirectoryListState ) ; this . props . sendPathUp ( pathToDirectory ) ; } @ autobind private handleKeyDown ( event ) { if ( this . state . creatingNewItem || this . state . renamingItem ) { return ; } if ( event . key . length === 1 ) { const indexToSelect = this . model . textFinder . addCharAndSearch ( event . key , this . nonHiddenDirectoryItems ) ; if ( indexToSelect >= 0 ) { this . setState ( { selectedIndex : indexToSelect } as IDirectoryListState ) ; } } } @ autobind private move ( direction ) { if ( direction === "up" ) { if ( this . state . selectedIndex > 0 ) { this . setState ( prevState => ( { selectedIndex : prevState . selectedIndex - 1 } as IDirectoryListState ) ) ; } } else { if ( this . state . selectedIndex < this . nonHiddenDirectoryItems . length - 1 ) { this . setState ( prevState => ( { selectedIndex : prevState . selectedIndex + 1 } as IDirectoryListState ) ) ; } } } @ autobind private navigateToPath ( filePath ) { if ( filePath !== this . props . path ) { this . props . sendPathUp ( filePath ) ; } } @ autobind private openGoto ( ) { if ( ! this . state . isGotoOpen ) { this . setState ( { isGotoOpen : true } as IDirectoryListState ) ; } } @ autobind private async pasteFromClipboard ( ) { const { directoryManager , settingsManager } = this . props ; const { clipboardAction , clipboardItems } = this . model ; if ( clipboardAction === "copy" ) { if ( ! clipboardItems ) { throw new LoggedError ( "Clipboard items is undefined" ) ; } Utils . trace ( `Requesting to copy ${ clipboardItems . map ( item => item . path ) . join ( ", " ) } to ${ this . props . path }` ) ; await directoryManager . copyItems ( clipboardItems , this . props . path ) ; this . setState ( { directoryItems : await directoryManager . listDirectory ( this . props . path , { hideUnixStyleHiddenItems : settingsManager . settings . windows . hideUnixStyleHiddenItems } ) } as IDirectoryListState ) ; this . props . statusNotifier . notify ( "Copied items" ) ; } else if ( clipboardAction === "cut" ) { if ( ! clipboardItems ) { throw new LoggedError ( "Clipboard items is undefined" ) ; } Utils . trace ( `Requesting to move ${ clipboardItems . map ( item => item . path ) . join ( ", " ) } to ${ path }` ) ; await directoryManager . moveItems ( clipboardItems , this . props . path ) ; this . setState ( { directoryItems : await directoryManager . listDirectory ( this . props . path , { hideUnixStyleHiddenItems : settingsManager . settings . windows . hideUnixStyleHiddenItems } ) } as IDirectoryListState ) ; this . props . statusNotifier . notify ( "Cut items" ) ; } } @ autobind private refreshAfterDelete ( ) { this . setState ( { itemDeleted : true , selectedIndex : 0 } as IDirectoryListState ) ; } @ autobind private async renameItem ( oldName ? , newName ? ) { if ( oldName && newName ) { Utils . trace ( `Requesting to rename item from ${ oldName } to ${ newName }` ) ; await this . props . directoryManager . renameItem ( oldName , newName , this . props . path ) ; } this . setState ( { renamingItem : false } as IDirectoryListState ) ; } @ autobind private scrollTo ( scrollToDirection ) { if ( scrollToDirection === "top" ) { this . setState ( { selectedIndex : 0 } as IDirectoryListState , ( ) => { this . context . scrollArea . scrollTop ( ) ; } ) ; } else { this . setState ( { selectedIndex : this . nonHiddenDirectoryItems . length - 1 } as IDirectoryListState , ( ) => { this . context . scrollArea . scrollBottom ( ) ; } ) ; } } @ autobind private selectItem ( itemToSelect ) { const index = this . nonHiddenDirectoryItems . findIndex ( item => item . name === itemToSelect . name ) ; this . setState ( { selectedIndex : index } as IDirectoryListState ) ; this . props . sendSelectedPaneUp ( this . props . id ) ; } @ autobind private async sendToTrash ( ) { const { settingsManager , directoryManager , statusNotifier } = this . props ; const chosenItems = this . selectedItems . length > 1 ? "the chosen items" : `'${ this . selectedItems [ 0 ] . name }'` ; const confirmTrash = settingsManager . settings . confirmation . requiredBeforeTrash ? confirmationDialog ( `Are you sure you want to send ${ chosenItems } to the trash?` ) : true ; this . keysTrapper && Utils . autoFocus ( this . keysTrapper ) ; if ( confirmTrash ) { Utils . trace ( `Requesting to trash ${ this . selectedItems . map ( item => item . path ) . join ( ", " ) }` ) ; await directoryManager . sendItemsToTrash ( this . selectedItems ) ; this . refreshAfterDelete ( ) ; statusNotifier . notify ( "Sent items to trash" ) ; } } @ autobind private setFocused ( ) { this . setState ( { isFocused : true } as IDirectoryListState ) ; } @ autobind private setUnFocused ( ) { this . setState ( { isFocused : false } as IDirectoryListState ) ; } private startWatcher ( ) { const { directoryManager , settingsManager } = this . props ; try { directoryManager . startWatching ( this . props . path , async ( ) => { this . setState ( { directoryItems : await directoryManager . listDirectory ( this . props . path , { hideUnixStyleHiddenItems : settingsManager . settings . windows . hideUnixStyleHiddenItems } ) } as IDirectoryListState ) ; } ) ; } catch { throw new DirectoryError ( "Could not set watcher" , this . props . path ) ; } } @ autobind private storeItemInClipboard ( action ) { this . model . itemClipboard = { clipboardAction : action , directoryItems : this . selectedItems } ; if ( action === "copy" ) { this . props . statusNotifier . notify ( "Copying item(s)" ) ; } else { this . props . statusNotifier . notify ( "Cutting item(s)" ) ; } } @ autobind private toggleItemChosen ( ) { const selectedItem = this . nonHiddenDirectoryItems [ this . state . selectedIndex ] ; if ( this . state . chosenItems . includes ( selectedItem ) ) { this . setState ( ( currentState ) => ( { chosenItems : currentState . chosenItems . filter ( item => item . name !== selectedItem . name ) } as IDirectoryListState ) ) ; } else { const chosenItems = List ( this . state . chosenItems ) . withMutations ( list => list . push ( selectedItem ) ) ; this . setState ( ( currentState ) => ( { chosenItems : chosenItems . toArray ( ) } as IDirectoryListState ) ) ; } } @ autobind private toggleShowHidden ( ) { if ( this . state . showHiddenItems ) { this . props . statusNotifier . notify ( "Hiding hidden items" ) ; } else { this . props . statusNotifier . notify ( "Showing hidden items" ) ; } this . setState ( prevState => ( { showHiddenItems : ! prevState . showHiddenItems } as IDirectoryListState ) ) ; } } param message returns function confirmationDialog ( message ) { const confirmIndex = 0 ; const cancelIndex = 1 ; const confirmation = dialog . showMessageBox ( { buttons : [ "OK" , "Cancel" ] , cancelId : cancelIndex , defaultId : cancelIndex , message , title : "Confirm deletion" , type : "warning" } ) ; return confirmation === confirmIndex ; } export default DirectoryList ;