import * as React from "react" ; import { IFocusedCellCoordinates } from "./common/cell" ; import { Clipboard } from "./common/clipboard" ; import { Direction } from "./common/direction" ; import { Grid } from "./common/grid" ; import * as FocusedCellUtils from "./common/internal/focusedCellUtils" ; import * as SelectionUtils from "./common/internal/selectionUtils" ; import { Region , RegionCardinality , Regions } from "./regions" ; import type { TableProps } from "./tableProps" ; import type { TableState , TableSnapshot } from "./tableState" ; export interface TableHandlers { handleSelection : ( selectedRegions : Region [ ] ) => void ; handleFocus : ( focusedCell : IFocusedCellCoordinates ) => void ; getEnabledSelectionHandler : ( selectionMode : RegionCardinality ) => ( selectedRegions : Region [ ] ) => void ; syncViewportPosition : ( snapshot : TableSnapshot ) => void ; } export class TableHotkeys { public constructor ( private props , private state , private grid , private tableHandlers ) { } public setGrid ( grid ) { this . grid = grid ; } public setProps ( props ) { this . props = props ; } public setState ( state ) { this . state = state ; } private selectAll = ( shouldUpdateFocusedCell ) => { const selectionHandler = this . tableHandlers . getEnabledSelectionHandler ( RegionCardinality . FULL_TABLE ) ; selectionHandler ( [ Regions . table ( ) ] ) ; if ( shouldUpdateFocusedCell ) { const newFocusedCellCoordinates = Regions . getFocusCellCoordinatesFromRegion ( Regions . table ( ) ) ; this . tableHandlers . handleFocus ( FocusedCellUtils . toFullCoordinates ( newFocusedCellCoordinates ) ) ; } } ; public handleSelectAllHotkey = ( e ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; this . selectAll ( false ) ; } ; public handleSelectionResizeUp = ( e ) => this . handleSelectionResize ( e , Direction . UP ) ; public handleSelectionResizeDown = ( e ) => this . handleSelectionResize ( e , Direction . DOWN ) ; public handleSelectionResizeLeft = ( e ) => this . handleSelectionResize ( e , Direction . LEFT ) ; public handleSelectionResizeRight = ( e ) => this . handleSelectionResize ( e , Direction . RIGHT ) ; private handleSelectionResize = ( e , direction ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; const { focusedCell , selectedRegions } = this . state ; if ( selectedRegions . length === 0 ) { return ; } const index = FocusedCellUtils . getFocusedOrLastSelectedIndex ( selectedRegions , focusedCell ) ; const region = selectedRegions [ index ] ; const nextRegion = SelectionUtils . resizeRegion ( region , direction , focusedCell ) ; this . updateSelectedRegionAtIndex ( nextRegion , index ) ; } ; /**
     * Replaces the selected region at the specified array index, with the
     * region provided.
     */ private updateSelectedRegionAtIndex ( region , index ) { const { children , numRows } = this . props ; const { selectedRegions } = this . state ; const numColumns = React . Children . count ( children ) ; const maxRowIndex = Math . max ( 0 , numRows - 1 ) ; const maxColumnIndex = Math . max ( 0 , numColumns - 1 ) ; const clampedNextRegion = Regions . clampRegion ( region , maxRowIndex , maxColumnIndex ) ; const nextSelectedRegions = Regions . update ( selectedRegions , clampedNextRegion , index ) ; this . tableHandlers . handleSelection ( nextSelectedRegions ) ; } public handleFocusMoveLeft = ( e ) => this . handleFocusMove ( e , "left" ) ; public handleFocusMoveLeftInternal = ( e ) => this . handleFocusMoveInternal ( e , "left" ) ; public handleFocusMoveRight = ( e ) => this . handleFocusMove ( e , "right" ) ; public handleFocusMoveRightInternal = ( e ) => this . handleFocusMoveInternal ( e , "right" ) ; public handleFocusMoveUp = ( e ) => this . handleFocusMove ( e , "up" ) ; public handleFocusMoveUpInternal = ( e ) => this . handleFocusMoveInternal ( e , "up" ) ; public handleFocusMoveDown = ( e ) => this . handleFocusMove ( e , "down" ) ; public handleFocusMoveDownInternal = ( e ) => this . handleFocusMoveInternal ( e , "down" ) ; private handleFocusMove = ( e , direction ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; const { focusedCell } = this . state ; if ( focusedCell == null ) { return ; } const newFocusedCell = { col : focusedCell . col , focusSelectionIndex : 0 , row : focusedCell . row , } ; switch ( direction ) { case "up" : newFocusedCell . row -= 1 ; break ; case "down" : newFocusedCell . row += 1 ; break ; case "left" : newFocusedCell . col -= 1 ; break ; case "right" : newFocusedCell . col += 1 ; break ; default : break ; } if ( newFocusedCell . row < 0 || newFocusedCell . row >= this . grid . numRows || newFocusedCell . col < 0 || newFocusedCell . col >= this . grid . numCols ) { return ; } const newSelectionRegions = [ Regions . cell ( newFocusedCell . row , newFocusedCell . col ) ] ; const { selectedRegionTransform } = this . props ; const transformedSelectionRegions = selectedRegionTransform != null ? newSelectionRegions . map ( region => selectedRegionTransform ( region , e ) ) : newSelectionRegions ; this . tableHandlers . handleSelection ( transformedSelectionRegions ) ; this . tableHandlers . handleFocus ( newFocusedCell ) ; this . scrollBodyToFocusedCell ( newFocusedCell ) ; } ; private handleFocusMoveInternal = ( e , direction ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; const { focusedCell , selectedRegions } = this . state ; if ( focusedCell == null ) { return ; } let newFocusedCell = { col : focusedCell . col , focusSelectionIndex : focusedCell . focusSelectionIndex , row : focusedCell . row , } ; if ( focusedCell . focusSelectionIndex == null && selectedRegions . length > 0 ) { const focusCellRegion = Regions . getCellRegionFromRegion ( selectedRegions [ 0 ] , this . grid . numRows , this . grid . numCols ) ; newFocusedCell = { col : focusCellRegion . cols [ 0 ] , focusSelectionIndex : 0 , row : focusCellRegion . rows [ 0 ] , } ; } else { if ( selectedRegions . length === 0 ) { this . handleFocusMove ( e , direction ) ; return ; } const focusCellRegion = Regions . getCellRegionFromRegion ( selectedRegions [ focusedCell . focusSelectionIndex ] , this . grid . numRows , this . grid . numCols ) ; if ( focusCellRegion . cols [ 0 ] === focusCellRegion . cols [ 1 ] && focusCellRegion . rows [ 0 ] === focusCellRegion . rows [ 1 ] && selectedRegions . length === 1 ) { this . handleFocusMove ( e , direction ) ; return ; } switch ( direction ) { case "up" : newFocusedCell = this . moveFocusCell ( "row" , "col" , true , newFocusedCell , focusCellRegion ) ; break ; case "left" : newFocusedCell = this . moveFocusCell ( "col" , "row" , true , newFocusedCell , focusCellRegion ) ; break ; case "down" : newFocusedCell = this . moveFocusCell ( "row" , "col" , false , newFocusedCell , focusCellRegion ) ; break ; case "right" : newFocusedCell = this . moveFocusCell ( "col" , "row" , false , newFocusedCell , focusCellRegion ) ; break ; default : break ; } } if ( newFocusedCell . row < 0 || newFocusedCell . row >= this . grid . numRows || newFocusedCell . col < 0 || newFocusedCell . col >= this . grid . numCols ) { return ; } this . tableHandlers . handleFocus ( newFocusedCell ) ; this . scrollBodyToFocusedCell ( newFocusedCell ) ; } ; private scrollBodyToFocusedCell = ( focusedCell ) => { const { row , col } = focusedCell ; const { viewportRect } = this . state ; const viewportBounds = { top : viewportRect . top , right : viewportRect . left + viewportRect . width , bottom : viewportRect . top + viewportRect . height , left : viewportRect . left , } ; const focusedCellBounds = { top : this . grid . getCumulativeHeightBefore ( row ) , right : this . grid . getCumulativeWidthAt ( col ) , bottom : this . grid . getCumulativeHeightAt ( row ) , left : this . grid . getCumulativeWidthBefore ( col ) , } ; const focusedCellWidth = focusedCellBounds . right - focusedCellBounds . left ; const focusedCellHeight = focusedCellBounds . bottom - focusedCellBounds . top ; const isFocusedCellWiderThanViewport = focusedCellWidth > viewportRect . width ; const isFocusedCellTallerThanViewport = focusedCellHeight > viewportRect . height ; const ss = { } ; if ( focusedCellBounds . top < viewportBounds . top || isFocusedCellTallerThanViewport ) { ss . nextScrollTop = Math . max ( 0 , focusedCellBounds . top - 1 ) ; } else if ( focusedCellBounds . bottom > viewportBounds . bottom ) { const scrollDelta = focusedCellBounds . bottom - viewportBounds . bottom ; ss . nextScrollTop = viewportBounds . top + scrollDelta ; } if ( focusedCellBounds . left < viewportBounds . left || isFocusedCellWiderThanViewport ) { ss . nextScrollLeft = Math . max ( 0 , focusedCellBounds . left - 1 ) ; } else if ( focusedCellBounds . right > viewportBounds . right ) { const scrollDelta = focusedCellBounds . right - viewportBounds . right ; ss . nextScrollLeft = viewportBounds . left + scrollDelta ; } this . tableHandlers . syncViewportPosition ( ss ) ; } ; private moveFocusCell ( primaryAxis , secondaryAxis , isUpOrLeft , newFocusedCell , focusCellRegion ) { const { selectedRegions } = this . state ; const primaryAxisPlural = primaryAxis === "row" ? "rows" : "cols" ; const secondaryAxisPlural = secondaryAxis === "row" ? "rows" : "cols" ; const movementDirection = isUpOrLeft ? - 1 : + 1 ; const regionIntervalIndex = isUpOrLeft ? 1 : 0 ; newFocusedCell [ primaryAxis ] += movementDirection ; const isPrimaryIndexOutOfBounds = isUpOrLeft ? newFocusedCell [ primaryAxis ] < focusCellRegion [ primaryAxisPlural ] [ 0 ] : newFocusedCell [ primaryAxis ] > focusCellRegion [ primaryAxisPlural ] [ 1 ] ; if ( isPrimaryIndexOutOfBounds ) { newFocusedCell [ primaryAxis ] = focusCellRegion [ primaryAxisPlural ] [ regionIntervalIndex ] ; newFocusedCell [ secondaryAxis ] += movementDirection ; const isSecondaryIndexOutOfBounds = isUpOrLeft ? newFocusedCell [ secondaryAxis ] < focusCellRegion [ secondaryAxisPlural ] [ 0 ] : newFocusedCell [ secondaryAxis ] > focusCellRegion [ secondaryAxisPlural ] [ 1 ] ; if ( isSecondaryIndexOutOfBounds ) { let newFocusCellSelectionIndex = newFocusedCell . focusSelectionIndex + movementDirection ; if ( isUpOrLeft ? newFocusCellSelectionIndex < 0 : newFocusCellSelectionIndex >= selectedRegions . length ) { newFocusCellSelectionIndex = isUpOrLeft ? selectedRegions . length - 1 : 0 ; } const newFocusCellRegion = Regions . getCellRegionFromRegion ( selectedRegions [ newFocusCellSelectionIndex ] , this . grid . numRows , this . grid . numCols ) ; newFocusedCell = { col : newFocusCellRegion . cols [ regionIntervalIndex ] , focusSelectionIndex : newFocusCellSelectionIndex , row : newFocusCellRegion . rows [ regionIntervalIndex ] , } ; } } return newFocusedCell ; } public handleCopy = ( e ) => { const { getCellClipboardData , onCopy } = this . props ; const { selectedRegions } = this . state ; if ( getCellClipboardData == null ) { return ; } e . preventDefault ( ) ; e . stopPropagation ( ) ; const cells = Regions . enumerateUniqueCells ( selectedRegions , this . grid . numRows , this . grid . numCols ) ; const sparse = Regions . sparseMapCells ( cells , getCellClipboardData ) ; if ( sparse != null ) { const success = Clipboard . copyCells ( sparse ) ; onCopy ?. ( success ) ; } } ; }