/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import { Box, gloss } from 'gloss'
import invariant from 'invariant'
import * as React from 'react'
import { ContextMenu } from '../ContextMenu'
import { Interactive, InteractiveProps } from '../Interactive'
import { SimpleText } from '../text/SimpleText'
import { DataColumns, DataType } from '../types'
import { DEFAULT_ROW_HEIGHT, SortOrder, TableColumnOrder, TableColumnSizes, TableOnColumnResize, TableOnSort } from './types'
import { isPercentage, normaliseColumnWidth } from './utils'
const RIGHT_RESIZABLE = { right: true }
class TableHeadColumn extends React.PureComponent<{
id: string
width: string | number
height?: number
sortable?: boolean
isResizable: boolean
leftHasResizer: boolean
hasFlex: boolean
sortOrder?: SortOrder
onSort?: TableOnSort
columnSizes: TableColumnSizes
onColumnResize?: TableOnColumnResize
children?: React.ReactNode
title?: string
}> {
ref: HTMLElement
static defaultProps = {
height: DEFAULT_ROW_HEIGHT,
}
onClick = () => {
const { id, onSort, sortOrder } = this.props
const direction =
sortOrder && sortOrder.key === id && sortOrder.direction === 'down' ? 'up' : 'down'
if (onSort) {
onSort({
direction,
key: id,
})
}
}
lastResize = Date.now()
onResize = (newWidth: number) => {
const { id, columnSizes, onColumnResize, width } = this.props
if (!onColumnResize) {
return
}
// throttle a bit
const last = this.lastResize
this.lastResize = Date.now()
if (Date.now() - last < 30) {
return
}
let normalizedWidth: number | string = newWidth
// normalise number to a percentage if we were originally passed a percentage
if (isPercentage(width)) {
const { parentElement } = this.ref
invariant(parentElement, 'expected there to be parentElement')
const parentWidth = parentElement.clientWidth
const { childNodes } = parentElement
const lastElem = childNodes[childNodes.length - 1]
const right =
lastElem instanceof HTMLElement ? lastElem.offsetLeft + lastElem.clientWidth + 1 : 0
if (right < parentWidth) {
normalizedWidth = calculatePercentage(parentWidth, newWidth)
}
}
onColumnResize({
...columnSizes,
[id]: normalizedWidth,
})
}
setRef = (ref: HTMLElement) => {
this.ref = ref
}
render() {
const { isResizable, sortable, width, height } = this.props
let { children } = this.props
children = {children}
if (isResizable) {
children = (
{children}
)
}
return (
{children}
)
}
}
// this will:
// 1. if no flex provided, assume that strings should flex double anything else
// 2. if any flex provided, default rest to flex 1
// 3. calculate the percentage width based on flexes
function calculateColumnSizes(columns: DataColumns): TableColumnSizes {
const values = Object.keys(columns).map(k => columns[k])
const isUncontrolled = values.some(x => typeof x.flex !== 'undefined')
const flexes = values.map(val => {
if (isUncontrolled) {
return !val.type || val.type === DataType.string ? 2 : 1
} else {
return val.flex || 1
}
})
const totalFlex = flexes.reduce((a, flex) => a + flex, 0)
const sizes = {}
for (const key of Object.keys(columns)) {
const flex = columns[key].flex
sizes[key] = (flex / totalFlex) * 100
}
return sizes
}
export class TableHead extends React.PureComponent<
{
columnOrder: TableColumnOrder
onColumnOrder?: (order: TableColumnOrder) => void
columns: DataColumns
sortOrder?: SortOrder
onSort?: TableOnSort
columnSizes?: TableColumnSizes
onColumnResize?: TableOnColumnResize
height?: number
},
{ columnSizes: TableColumnSizes }
> {
state = {
columnSizes: this.props.columnSizes || calculateColumnSizes(this.props.columns),
}
static getDerivedStateFromProps(props) {
const columnSizes = props.columnSizes || calculateColumnSizes(props.columns)
return {
columnSizes,
}
}
buildContextMenu = (): any[] => {
const visibles = this.props.columnOrder
.map(c => (c.visible ? c.key : null))
.filter(Boolean)
.reduce((acc, cv) => {
acc.add(cv)
return acc
}, new Set())
return Object.keys(this.props.columns).map(key => {
const visible = visibles.has(key)
return {
label: this.props.columns[key].value,
click: () => {
const { onColumnOrder, columnOrder } = this.props
if (onColumnOrder) {
const newOrder = columnOrder.slice()
let hasVisibleItem = false
for (let i = 0; i < newOrder.length; i++) {
const info = newOrder[i]
if (info.key === key) {
newOrder[i] = { key, visible: !visible }
}
hasVisibleItem = hasVisibleItem || newOrder[i].visible
}
// Dont allow hiding all columns
if (hasVisibleItem) {
onColumnOrder(newOrder)
}
}
},
type: 'checkbox',
checked: visible,
}
})
}
render() {
const { columnOrder, columns, onColumnResize, onSort, sortOrder, height } = this.props
const { columnSizes } = this.state
const elems = []
let hasFlex = false
for (const column of columnOrder) {
if (column.visible && columnSizes[column.key] === 'flex') {
hasFlex = true
break
}
}
let lastResizable = true
const colElems = {}
for (const column of columnOrder) {
if (!column.visible) {
continue
}
const key = column.key
const col = columns[key]
let arrow
if (col.sortable === true && sortOrder && sortOrder.key === key) {
arrow = (
{sortOrder.direction === 'up' ? '▲' : '▼'}
)
}
const width = normaliseColumnWidth(columnSizes[key])
const isResizable = col.resizable !== false
const elem = (
{col.value}
{arrow}
)
elems.push(elem)
colElems[key] = elem
lastResizable = isResizable
}
return (
{elems}
)
}
}
const TableHeaderArrow = gloss(SimpleText, {
display: 'block',
opacity: 0.6,
})
const TableHeadColumnText = gloss(SimpleText, {
size: 0.85,
alpha: 0.8,
display: 'inline-block',
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontWeight: 500,
})
const TableHeaderColumnInteractive = gloss(Interactive, {
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
})
const TableHeaderColumnContainer = gloss(Box, {
flexDirection: 'row',
padding: '0 8px',
margin: ['auto', 0],
alignItems: 'center',
})
const TableHeadContainer = gloss(Box, {
flexDirection: 'row',
flexShrink: 0,
left: 0,
overflow: 'hidden',
position: 'sticky',
right: 0,
textAlign: 'left',
top: 0,
zIndex: 2,
}).theme(props => ({
borderBottom: [1, props.borderColorLight],
}))
const TableHeadColumnContainer = gloss(Box, {
flexDirection: 'row',
justifyContent: 'space-between',
position: 'relative',
userSelect: 'none',
'&:after': {
position: 'absolute',
content: '" "',
right: 0,
top: '15%',
height: '75%',
width: 1,
},
'&:last-child::after': {
display: 'none',
},
}).theme(props => ({
lineHeight: props.height || `${DEFAULT_ROW_HEIGHT}px`,
height: props.height || DEFAULT_ROW_HEIGHT,
background: props.tableHeadBackground || props.backgroundStrong,
flexShrink: props.width === 'flex' ? 1 : 0,
width: props.width === 'flex' ? '100%' : props.width,
'&:after': {
background: props.borderColor,
},
}))
function calculatePercentage(parentWidth: number, selfWidth: number): string {
return `${(100 / parentWidth) * selfWidth}%`
}