import React , { useState } from 'react' ; import { observer } from 'mobx-react' ; import { getParent } from 'mobx-state-tree' ; import { grey , indigo } from '@material-ui/core/colors' ; import { Checkbox , IconButton , Tooltip , FormControlLabel , makeStyles , } from '@material-ui/core' ; import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp' ; import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown' ; import CropFreeIcon from '@material-ui/icons/CropFree' ; import ArrowDropDown from '@material-ui/icons/ArrowDropDown' ; import ColumnMenu from './ColumnMenu' ; import RowMenu from './RowMenu' ; export function numToColName ( num ) { function letterFor ( n ) { return String . fromCharCode ( n + 65 ) ; } if ( num >= 0 ) { if ( num < 26 ) { return letterFor ( num ) ; } if ( num < 27 * 26 ) { return letterFor ( Math . floor ( num / 26 - 1 ) ) + letterFor ( num % 26 ) ; } } throw new RangeError ( 'column number out of range' ) ; } const useStyles = makeStyles ( theme => { return { root : { position : 'relative' , marginBottom : theme . spacing ( 1 ) , background : grey [ 500 ] , overflow : 'auto' , } , dataTable : { borderCollapse : 'collapse' , borderSpacing : 0 , boxSizing : 'border-box' , '& td' : { border : `1px solid ${ grey [ 300 ] }` , padding : '0.2rem' , maxWidth : '50em' , overflow : 'hidden' , textOverflow : 'ellipsis' , } , } , dataTableBody : { background : 'white' , } , rowNumCell : { background : grey [ 200 ] , textAlign : 'left' , border : `1px solid ${ grey [ 300 ] }` , position : 'relative' , padding : '0 2px 0 0' , whiteSpace : 'nowrap' , userSelect : 'none' , } , rowNumber : { fontWeight : 'normal' , display : 'inline-block' , flex : 'none' , paddingRight : '20px' , margin : 0 , whiteSpace : 'nowrap' , } , rowMenuButton : { padding : 0 , margin : 0 , position : 'absolute' , right : 0 , display : 'inline-block' , whiteSpace : 'nowrap' , flex : 'none' , } , rowMenuButtonIcon : { } , rowSelector : { position : 'relative' , top : '-2px' , margin : 0 , padding : '0 0.2rem' , } , columnHead : { fontWeight : 'normal' , background : grey [ 200 ] , border : `1px solid ${ grey [ 300 ] }` , position : 'sticky' , top : '-1px' , zIndex : 2 , whiteSpace : 'nowrap' , padding : [ [ 0 , theme . spacing ( 1 ) ] ] , } , sortIndicator : { position : 'relative' , top : '0.2rem' , fontSize : '1rem' , } , columnButtonContainer : { display : 'none' , position : 'absolute' , right : 0 , top : 0 , background : grey [ 100 ] , height : '100%' , boxSizing : 'border-box' , borderLeft : `1px solid ${ grey [ 300 ] }` , } , columnButton : { padding : 0 , } , topLeftCorner : { background : grey [ 300 ] , position : 'sticky' , top : '-1px' , zIndex : 2 , minWidth : theme . spacing ( 2 ) , textAlign : 'left' , } , dataRowSelected : { background : indigo [ 100 ] , '& th' : { background : indigo [ 100 ] , } , } , emptyMessage : { captionSide : 'bottom' } , } ; } ) ; const CellData = observer ( ( { cell , spreadsheetModel , columnNumber } ) => { const { dataType } = spreadsheetModel . columns . get ( columnNumber ) ; if ( dataType . DataCellReactComponent ) { return ( < dataType . DataCellReactComponent cell = { cell } dataType = { dataType } columnNumber = { columnNumber } spreadsheet = { spreadsheetModel } / > ) ; } return cell . text ; } ) ; const DataRow = observer ( ( { rowModel , rowNumber , spreadsheetModel } ) => { const classes = useStyles ( ) ; const { hideRowSelection , columnDisplayOrder } = spreadsheetModel ; let rowClass = classes . dataRow ; if ( rowModel . isSelected ) { rowClass += ` ${ classes . dataRowSelected }` ; } function labelClick ( evt ) { rowModel . toggleSelect ( ) ; evt . stopPropagation ( ) ; evt . preventDefault ( ) ; } function rowButtonClick ( event ) { spreadsheetModel . setRowMenuPosition ( { anchorEl : event . currentTarget , rowNumber , } ) ; event . preventDefault ( ) ; event . stopPropagation ( ) ; } return ( < tr className = { rowClass } > < th className = { classes . rowNumCell } onClick = { labelClick } > < FormControlLabel className = { classes . rowNumber } control = { hideRowSelection ? null : ( < Checkbox className = { classes . rowSelector } checked = { rowModel . isSelected } onClick = { labelClick } / > ) } label = { rowModel . id } / > < IconButton className = { classes . rowMenuButton } onClick = { rowButtonClick } color = "secondary" > < ArrowDropDown className = { classes . rowMenuButtonIcon } /> < /IconButton> < /th> { columnDisplayOrder . map ( colNumber => ( < td key = { colNumber } > < CellData cell = { rowModel . cellsWithDerived [ colNumber ] } spreadsheetModel = { spreadsheetModel } columnNumber = { colNumber } / > < /td>))} < /tr>); } ) ; function SortIndicator ( { model , columnNumber } ) { const classes = useStyles ( ) ; const sortSpec = model . sortColumns . find ( c => c . columnNumber === columnNumber ) ; if ( sortSpec ) { const { descending } = sortSpec ; return descending ? ( < KeyboardArrowUpIcon className = { classes . sortIndicator } / > ) : ( < KeyboardArrowDownIcon className = { classes . sortIndicator } / > ) ; } return null ; } const DataTableBody = observer ( ( { rows , spreadsheetModel , page , rowsPerPage } ) => { const classes = useStyles ( ) ; return ( < tbody className = { classes . dataTableBody } > { rows . slice ( rowsPerPage * page , rowsPerPage * ( page + 1 ) ) . map ( row => ( < DataRow key = { row . id } rowNumber = { row . id } spreadsheetModel = { spreadsheetModel } rowModel = { row } / > ) ) } < /tbody>); } ) ; const DataTable = observer ( ( { model , page , rowsPerPage } ) => { const { columnDisplayOrder , columns , hasColumnNames , rowSet } = model ; const classes = useStyles ( ) ; const [ currentColumnMenu , setColumnMenu ] = useState ( null ) ; function columnButtonClick ( colNumber , evt ) { setColumnMenu ( { colNumber , anchorEl : evt . currentTarget , } ) ; } const [ currentHoveredColumn , setHoveredColumn ] = useState ( null ) ; function columnHeaderMouseOver ( colNumber ) { setHoveredColumn ( colNumber ) ; } function columnHeaderMouseOut ( ) { setHoveredColumn ( null ) ; } const totalRows = rowSet . count ; const rows = rowSet . sortedFilteredRows ; return ( < > < ColumnMenu viewModel = { getParent ( model ) } spreadsheetModel = { model } currentColumnMenu = { currentColumnMenu } setColumnMenu = { setColumnMenu } / > < RowMenu viewModel = { getParent ( model ) } spreadsheetModel = { model } / > < table className = { classes . dataTable } > < thead > < tr > < th className = { classes . topLeftCorner } > < Tooltip title = "Unselect all" placement = "right" > < span > < IconButton className = { classes . unselectAllButton } onClick = { model . unselectAll } disabled = { ! model . rowSet . selectedCount } color = "secondary" > < CropFreeIcon className = { classes . columnButtonIcon } / > < /IconButton> < /span> < /Tooltip> < /th> { columnDisplayOrder . map ( colNumber => ( < th className = { classes . columnHead } key = { colNumber } onMouseOver = { columnHeaderMouseOver . bind ( null , colNumber ) } onMouseOut = { columnHeaderMouseOut . bind ( null , colNumber ) } > < SortIndicator model = { model } columnNumber = { colNumber } / > { ( hasColumnNames && columns . get ( colNumber ) . name ) || numToColName ( colNumber ) } < div className = { classes . columnButtonContainer } style = { { display : currentHoveredColumn === colNumber || ( currentColumnMenu && currentColumnMenu . colNumber === colNumber ) ? 'block' : 'none' , } } > < IconButton className = { classes . columnButton } onClick = { columnButtonClick . bind ( null , colNumber ) } color = "secondary" > < ArrowDropDown className = { classes . columnButtonIcon } /> < /IconButton> < /div> < /th>))} < /tr> < /thead> < DataTableBody rows = { rows } spreadsheetModel = { model } page = { page } rowsPerPage = { rowsPerPage } / > { ! rows . length ? ( < caption className = { classes . emptyMessage } > { totalRows ? 'no rows match criteria' : 'no rows present' } < /caption>) : null} < /table> < />); } ) ; const Spreadsheet = observer ( ( { model , height , page , rowsPerPage } ) => { const classes = useStyles ( ) ; return ( < div className = { classes . root } style = { { height } } > { model && model . rowSet && model . rowSet . isLoaded && model . initialized ? ( < DataTable model = { model } page = { page } rowsPerPage = { rowsPerPage } / > ) : ( < div > Loading ... < /div>)} < /div>); } ) ; export default Spreadsheet ;