table
Summary: _typescript_ Reviewed By: priteshrnandgaonkar Differential Revision: D16807180 fbshipit-source-id: dcba794351eee69c0574dc224cf7bd2732bea447
This commit is contained in:
committed by
Facebook Github Bot
parent
62a204bdbe
commit
4c4169063d
318
src/ui/components/table/TableHead.tsx
Normal file
318
src/ui/components/table/TableHead.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* 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 {
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
TableOnColumnResize,
|
||||
TableOnSort,
|
||||
TableRowSortOrder,
|
||||
} from './types';
|
||||
import {normaliseColumnWidth, isPercentage} from './utils';
|
||||
import {PureComponent} from 'react';
|
||||
import ContextMenu from '../ContextMenu';
|
||||
import Interactive from '../Interactive';
|
||||
import styled from 'react-emotion';
|
||||
import {colors} from '../colors';
|
||||
import FlexRow from '../FlexRow';
|
||||
import invariant from 'invariant';
|
||||
import {MenuItemConstructorOptions} from 'electron';
|
||||
import React from 'react';
|
||||
|
||||
const TableHeaderArrow = styled('span')({
|
||||
float: 'right',
|
||||
});
|
||||
|
||||
const TableHeaderColumnInteractive = styled(Interactive)({
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const TableHeaderColumnContainer = styled('div')({
|
||||
padding: '0 8px',
|
||||
});
|
||||
|
||||
const TableHeadContainer = styled(FlexRow)(
|
||||
(props: {horizontallyScrollable?: boolean}) => ({
|
||||
borderBottom: `1px solid ${colors.sectionHeaderBorder}`,
|
||||
color: colors.light50,
|
||||
flexShrink: 0,
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
right: 0,
|
||||
textAlign: 'left',
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
minWidth: props.horizontallyScrollable ? 'min-content' : 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const TableHeadColumnContainer = styled('div')(
|
||||
(props: {width: string | number}) => ({
|
||||
position: 'relative',
|
||||
backgroundColor: colors.white,
|
||||
flexShrink: props.width === 'flex' ? 1 : 0,
|
||||
height: 23,
|
||||
lineHeight: '23px',
|
||||
fontSize: '0.85em',
|
||||
fontWeight: 500,
|
||||
width: props.width === 'flex' ? '100%' : props.width,
|
||||
'&::after': {
|
||||
position: 'absolute',
|
||||
content: '""',
|
||||
right: 0,
|
||||
top: 5,
|
||||
height: 13,
|
||||
width: 1,
|
||||
background: colors.light15,
|
||||
},
|
||||
'&:last-child::after': {
|
||||
display: 'none',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const RIGHT_RESIZABLE = {right: true};
|
||||
|
||||
function calculatePercentage(parentWidth: number, selfWidth: number): string {
|
||||
return `${(100 / parentWidth) * selfWidth}%`;
|
||||
}
|
||||
|
||||
class TableHeadColumn extends PureComponent<{
|
||||
id: string;
|
||||
width: string | number;
|
||||
sortable?: boolean;
|
||||
isResizable: boolean;
|
||||
leftHasResizer: boolean;
|
||||
hasFlex: boolean;
|
||||
sortOrder?: TableRowSortOrder;
|
||||
onSort?: TableOnSort;
|
||||
columnSizes: TableColumnSizes;
|
||||
onColumnResize?: TableOnColumnResize;
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
horizontallyScrollable?: boolean;
|
||||
}> {
|
||||
ref: HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.horizontallyScrollable) {
|
||||
// measure initial width
|
||||
this.onResize(this.ref.offsetWidth);
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const {id, onSort, sortOrder} = this.props;
|
||||
|
||||
const direction =
|
||||
sortOrder && sortOrder.key === id && sortOrder.direction === 'down'
|
||||
? 'up'
|
||||
: 'down';
|
||||
|
||||
if (onSort) {
|
||||
onSort({
|
||||
direction,
|
||||
key: id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onResize = (newWidth: number) => {
|
||||
const {id, onColumnResize, width} = this.props;
|
||||
if (!onColumnResize) {
|
||||
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(id, normalizedWidth);
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isResizable, sortable, width, title} = this.props;
|
||||
let {children} = this.props;
|
||||
children = (
|
||||
<TableHeaderColumnContainer>{children}</TableHeaderColumnContainer>
|
||||
);
|
||||
|
||||
if (isResizable) {
|
||||
children = (
|
||||
<TableHeaderColumnInteractive
|
||||
grow={true}
|
||||
resizable={RIGHT_RESIZABLE}
|
||||
onResize={this.onResize}
|
||||
minWidth={20}>
|
||||
{children}
|
||||
</TableHeaderColumnInteractive>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeadColumnContainer
|
||||
width={width}
|
||||
title={title}
|
||||
onClick={sortable === true ? this.onClick : undefined}
|
||||
innerRef={this.setRef}>
|
||||
{children}
|
||||
</TableHeadColumnContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class TableHead extends PureComponent<{
|
||||
columnOrder: TableColumnOrder;
|
||||
onColumnOrder?: (order: TableColumnOrder) => void;
|
||||
columns: TableColumns;
|
||||
sortOrder?: TableRowSortOrder;
|
||||
onSort?: TableOnSort;
|
||||
columnSizes: TableColumnSizes;
|
||||
onColumnResize?: TableOnColumnResize;
|
||||
horizontallyScrollable?: boolean;
|
||||
}> {
|
||||
buildContextMenu = (): MenuItemConstructorOptions[] => {
|
||||
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' as 'checkbox',
|
||||
checked: visible,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
columnOrder,
|
||||
columns,
|
||||
columnSizes,
|
||||
onColumnResize,
|
||||
onSort,
|
||||
sortOrder,
|
||||
horizontallyScrollable,
|
||||
} = this.props;
|
||||
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 = (
|
||||
<TableHeaderArrow>
|
||||
{sortOrder.direction === 'up' ? '▲' : '▼'}
|
||||
</TableHeaderArrow>
|
||||
);
|
||||
}
|
||||
|
||||
const width = normaliseColumnWidth(columnSizes[key]);
|
||||
const isResizable = col.resizable !== false;
|
||||
|
||||
const elem = (
|
||||
<TableHeadColumn
|
||||
key={key}
|
||||
id={key}
|
||||
hasFlex={hasFlex}
|
||||
isResizable={isResizable}
|
||||
leftHasResizer={lastResizable}
|
||||
width={width}
|
||||
sortable={col.sortable}
|
||||
sortOrder={sortOrder}
|
||||
onSort={onSort}
|
||||
columnSizes={columnSizes}
|
||||
onColumnResize={onColumnResize}
|
||||
title={key}
|
||||
horizontallyScrollable={horizontallyScrollable}>
|
||||
{col.value}
|
||||
{arrow}
|
||||
</TableHeadColumn>
|
||||
);
|
||||
|
||||
elems.push(elem);
|
||||
|
||||
colElems[key] = elem;
|
||||
|
||||
lastResizable = isResizable;
|
||||
}
|
||||
return (
|
||||
<ContextMenu buildItems={this.buildContextMenu}>
|
||||
<TableHeadContainer horizontallyScrollable={horizontallyScrollable}>
|
||||
{elems}
|
||||
</TableHeadContainer>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user