import { SideBarStateContextShape } from 'containers/SideBarState' ; import { GetCreatedMethod , MethodCreator } from 'driver/connect' ; import { platform } from 'platforms' ; import { Config } from 'utils/config/helper' ; import * as DOMHelper from 'utils/DOMHelper' ; import { isOpenInNewWindowClick , OperatingSystems , os } from 'utils/general' ; import { loadWithPJAX } from 'utils/hooks/usePJAX' ; import { VisibleNodes , VisibleNodesGenerator } from 'utils/VisibleNodesGenerator' ; export type Props = { metaData : MetaData ; freeze : boolean ; accessToken : string | undefined ; config : Config ; catchNetworkErrors : < T > ( fn : ( ) => T ) => Promise < T | undefined > ; } ; export type ConnectorState = { visibleNodesGenerator : VisibleNodesGenerator | null ; visibleNodes : VisibleNodes | null ; searchKey : string ; searched : boolean ; defer : boolean ; handleKeyDown : GetCreatedMethod < typeof handleKeyDown > ; updateSearchKey : GetCreatedMethod < typeof updateSearchKey > ; onNodeClick : GetCreatedMethod < typeof onNodeClick > ; onFocusSearchBar : GetCreatedMethod < typeof onFocusSearchBar > ; setUpTree : GetCreatedMethod < typeof setUpTree > ; goTo : GetCreatedMethod < typeof goTo > ; expandTo : GetCreatedMethod < typeof expandTo > ; } ; function getVisibleParentNode ( nodes , focusedNode ) { let index = nodes . findIndex ( node => node . path === focusedNode . path ) - 1 ; while ( index >= 0 ) { if ( nodes [ index ] . contents ?. includes ( focusedNode ) ) { return nodes [ index ] ; } -- index ; } } type BoundMethodCreator < Args extends any [ ] = [ ] > = MethodCreator < Props , ConnectorState , Args > ; export const setUpTree = dispatch => ( { stateContext , metaData , config } ) => { const { props : { catchNetworkErrors } , } = dispatch . get ( ) ; catchNetworkErrors ( async ( ) => { const { userName , repoName , branchName } = metaData ; stateContext . onChange ( 'tree-loading' ) ; const { root : treeRoot , defer = false } = await platform . getTreeData ( { branchName : branchName , userName , repoName , } , '/' , true , config . accessToken ) ; stateContext . onChange ( 'tree-rendering' ) ; dispatch . set ( { defer } ) ; const visibleNodesGenerator = new VisibleNodesGenerator ( { root : treeRoot , compress : config . compressSingletonFolder , async getTreeData ( path ) { const { root } = await platform . getTreeData ( metaData , path , false , config . accessToken ) ; return root ; } , } ) ; dispatch . set ( { visibleNodesGenerator } ) ; visibleNodesGenerator . onUpdate ( visibleNodes => dispatch . set ( { visibleNodes } ) ) ; if ( platform . shouldExpandAll ?. ( ) ) { const unsubscribe = visibleNodesGenerator . onUpdate ( visibleNodes => { unsubscribe ( ) ; visibleNodes . nodes . forEach ( node => dispatch . call ( toggleNodeExpansion , node , { recursive : true } ) ) ; } ) ; } else { const targetPath = platform . getCurrentPath ( metaData . branchName ) ; if ( targetPath ) dispatch . call ( goTo , targetPath ) ; } stateContext . onChange ( 'tree-rendered' ) ; } ) ; } ; export const handleKeyDown = dispatch => event => { const { state : { searched , visibleNodes } , } = dispatch . get ( ) ; if ( ! visibleNodes ) return ; const { nodes , focusedNode , expandedNodes } = visibleNodes ; function handleVerticalMove ( index ) { if ( 0 <= index && index < nodes . length ) { DOMHelper . focusFileExplorer ( ) ; dispatch . call ( focusNode , nodes [ index ] ) ; } else { DOMHelper . focusSearchInput ( ) ; dispatch . call ( focusNode , null ) ; } } const { key } = event ; let muteEvent = true ; if ( focusedNode ) { const focusedNodeIndex = nodes . findIndex ( node => node . path === focusedNode . path ) ; switch ( key ) { case 'ArrowUp' : handleVerticalMove ( focusedNodeIndex - 1 ) ; break ; case 'ArrowDown' : handleVerticalMove ( focusedNodeIndex + 1 ) ; break ; case 'ArrowLeft' : if ( wouldBlockHistoryNavigation ( event ) ) { muteEvent = false ; break ; } if ( expandedNodes . has ( focusedNode . path ) ) { dispatch . call ( toggleNodeExpansion , focusedNode , { recursive : event . altKey } ) ; } else { const parentNode = getVisibleParentNode ( nodes , focusedNode ) ; if ( parentNode ) { dispatch . call ( focusNode , parentNode ) ; } } break ; case 'ArrowRight' : if ( wouldBlockHistoryNavigation ( event ) ) { muteEvent = false ; break ; } if ( focusedNode . type === 'tree' ) { if ( expandedNodes . has ( focusedNode . path ) ) { const nextNode = nodes [ focusedNodeIndex + 1 ] ; if ( focusedNode . contents ?. includes ( nextNode ) ) { dispatch . call ( focusNode , nextNode ) ; } } else { dispatch . call ( toggleNodeExpansion , focusedNode , { recursive : event . altKey } ) ; } } else if ( focusedNode . type === 'blob' ) { const focusedNodeElement = DOMHelper . findNodeElement ( focusedNode , event . currentTarget ) ; if ( focusedNodeElement && focusedNode . url ) loadWithPJAX ( focusedNode . url , focusedNodeElement ) ; } else if ( focusedNode . type === 'commit' ) { window . open ( focusedNode . url ) ; } break ; case 'Enter' : if ( searched ) { dispatch . call ( goTo , focusedNode . path . split ( '/' ) ) ; } else { if ( focusedNode . type === 'tree' ) { dispatch . call ( toggleNodeExpansion , focusedNode , { recursive : event . altKey } ) ; } else if ( focusedNode . type === 'blob' ) { const focusedNodeElement = DOMHelper . findNodeElement ( focusedNode , event . currentTarget ) ; if ( focusedNodeElement && focusedNode . url ) loadWithPJAX ( focusedNode . url , focusedNodeElement ) ; } else if ( focusedNode . type === 'commit' ) { window . open ( focusedNode . url ) ; } } break ; default : muteEvent = false ; } if ( muteEvent ) { event . preventDefault ( ) ; } } else { if ( nodes . length ) { switch ( key ) { case 'ArrowDown' : DOMHelper . focusFileExplorer ( ) ; dispatch . call ( focusNode , nodes [ 0 ] ) ; break ; case 'ArrowUp' : DOMHelper . focusFileExplorer ( ) ; dispatch . call ( focusNode , nodes [ nodes . length - 1 ] ) ; break ; default : muteEvent = false ; } if ( muteEvent ) { event . preventDefault ( ) ; } } } } ; export const onFocusSearchBar = dispatch => ( ) => dispatch . call ( focusNode , null ) ; export const updateSearchKey = dispatch => searchKey => { dispatch . set ( { searchKey , searched : searchKey !== '' } ) ; } ; export const goTo = dispatch => path => { const { state : { visibleNodesGenerator } , } = dispatch . get ( ) ; if ( ! visibleNodesGenerator ) return ; dispatch . call ( updateSearchKey , '' ) ; visibleNodesGenerator . search ( null ) ; visibleNodesGenerator . onNextUpdate ( ( ) => { dispatch . call ( expandTo , path ) ; } ) ; } ; export const setExpand = dispatch => async ( node , expand = false ) => { const { state : { visibleNodesGenerator } , } = dispatch . get ( ) ; if ( ! visibleNodesGenerator ) return ; await visibleNodesGenerator . setExpand ( node , expand ) ; dispatch . call ( focusNode , node ) ; } ; export const toggleNodeExpansion = dispatch => async ( node , { recursive = false } ) => { const { state : { visibleNodesGenerator } , } = dispatch . get ( ) ; if ( ! visibleNodesGenerator ) return ; if ( node . type === 'tree' ) { visibleNodesGenerator . focusNode ( node ) ; await visibleNodesGenerator . toggleExpand ( node , recursive ) ; } } ; export const focusNode = dispatch => ( node ) => { const { state : { visibleNodesGenerator } , } = dispatch . get ( ) ; if ( ! visibleNodesGenerator ) return ; visibleNodesGenerator . focusNode ( node ) ; } ; export const onNodeClick = dispatch => ( event , node ) => { if ( isOpenInNewWindowClick ( event ) ) return ; const preventDefault = ! ( node . type === 'blob' && node . url ?. includes ( '#' ) ) ; if ( preventDefault ) event . preventDefault ( ) ; if ( node . type === 'tree' ) { const { props : { config : { recursiveToggleFolder } , } , } = dispatch . get ( ) ; const recursive = ( recursiveToggleFolder === 'shift' && event . shiftKey ) || ( recursiveToggleFolder === 'alt' && event . altKey ) ; dispatch . call ( toggleNodeExpansion , node , { recursive } ) ; } else if ( node . type === 'blob' ) { dispatch . call ( focusNode , node ) ; if ( node . url && ! node . url . includes ( '#' ) ) { loadWithPJAX ( node . url , event . currentTarget ) ; } } else if ( node . type === 'commit' ) { if ( node . url ) { window . open ( node . url , '_blank' ) ; } } } ; export const expandTo = dispatch => async ( currentPath ) => { const { state : { visibleNodesGenerator } , } = dispatch . get ( ) ; if ( ! visibleNodesGenerator ) return ; const nodeExpandedTo = await visibleNodesGenerator . expandTo ( currentPath . join ( '/' ) ) ; if ( nodeExpandedTo ) { visibleNodesGenerator . focusNode ( nodeExpandedTo ) ; } } ; function wouldBlockHistoryNavigation ( event ) { return ( ( os === OperatingSystems . macOS && event . metaKey ) || ( os !== OperatingSystems . macOS && event . altKey ) ) ; }