/** * 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}%` }