TeaWeb/shared/js/ui/react-elements/Table.tsx
2020-12-21 20:07:09 +01:00

203 lines
No EOL
7.2 KiB
TypeScript

import * as React from "react";
import {ReactElement} from "react";
const cssStyle = require("./Table.scss");
export interface TableColumn {
name: string;
header: () => ReactElement | ReactElement[];
width?: number;
fixedWidth?: string;
className?: string;
}
export interface TableRow<T = any> {
columns: {[key: string]: () => ReactElement | ReactElement[]};
className?: string;
userData?: T;
}
export interface TableProperties {
columns: TableColumn[];
rows: TableRow[];
className?: string;
headerClassName?: string;
bodyClassName?: string;
bodyOverlayOnly?: boolean;
bodyOverlay?: () => ReactElement;
hiddenColumns?: string[];
onHeaderContextMenu?: (event: React.MouseEvent) => void;
onBodyContextMenu?: (event: React.MouseEvent) => void;
onDrop?: (event: React.DragEvent) => void;
onDragOver?: (event: React.DragEvent) => void;
renderRow?: (row: TableRow, columns: TableColumn[], uniqueId: string) => React.ReactElement<TableRowElement>;
}
export interface TableState {
hiddenColumns: string[];
}
export interface TableRowProperties {
columns: TableColumn[];
rowData: TableRow;
}
export interface TableRowState {
hidden?: boolean;
}
export class TableRowElement extends React.Component<TableRowProperties & React.HTMLProps<HTMLDivElement>, TableRowState> {
constructor(props) {
super(props);
this.state = {};
}
render() {
if(this.state.hidden)
return null;
let totalWidth = this.props.columns.map(e => e.width | 0).reduce((a, b) => a + b, 0);
if(totalWidth === 0)
totalWidth = 1;
const properties = Object.assign({}, this.props) as any;
delete properties.rowData;
delete properties.columns;
properties.className = (properties.className || "") + " " + cssStyle.row;
const children = Array.isArray(this.props.children) ? this.props.children : typeof this.props.children !== "undefined" ? [this.props.children] : [];
return React.createElement("div", properties, ...this.props.columns.map(column => {
const supplier = this.props.rowData.columns[column.name];
if(column.width) {
return (
<div key={"tr-" + column.name}
className={cssStyle.dynamicColumn + " " + (column.className || "")}
style={{width: (column.width * 100 / totalWidth) + "%"}}>
{supplier ? supplier() : undefined}
</div>
);
} else if(column.fixedWidth) {
return (
<div key={"th-" + column.name}
className={cssStyle.fixedColumn + " " + (column.className || "")}
style={{width: column.fixedWidth}}>
{supplier ? supplier() : undefined}
</div>
);
}
}), ...children);
}
}
export class Table extends React.Component<TableProperties, TableState> {
private rowIndex = 0;
private refHeader = React.createRef<HTMLDivElement>();
private refHiddenHeader = React.createRef<HTMLDivElement>();
private refBody = React.createRef<HTMLDivElement>();
private lastHeaderHeight = 20;
private lastScrollbarWidth = 20;
constructor(props) {
super(props);
this.state = {
hiddenColumns: this.props.hiddenColumns || []
};
}
render() {
const columns = this.props.columns.filter(e => this.state.hiddenColumns.findIndex(b => e.name === b) === -1);
let totalWidth = columns.map(e => e.width | 0).reduce((a, b) => a + b, 0);
if(totalWidth === 0)
totalWidth = 1;
const rowRenderer = this.props.renderRow || ((row, columns, uniqueId) => {
return <TableRowElement key={uniqueId} rowData={row} columns={columns} />;
});
let body;
if(this.props.bodyOverlayOnly) {
body = this.props.bodyOverlay ? this.props.bodyOverlay() : undefined;
} else {
body = this.props.rows.map((row: TableRow & { __rowIndex: number }) => {
if(typeof row.__rowIndex !== "number")
row.__rowIndex = ++this.rowIndex;
return rowRenderer(row, columns, "tr-" + row.__rowIndex);
});
if(this.props.bodyOverlay) {
body.push(this.props.bodyOverlay());
}
}
return (
<div
className={cssStyle.container + " " + (this.props.className || " ")}
onDrop={e => this.props.onDrop && this.props.onDrop(e)}
onDragOver={e => this.props.onDragOver && this.props.onDragOver(e)}
>
<div
ref={this.refHeader}
className={cssStyle.header + " " + (this.props.headerClassName || " ")}
style={{right: this.lastScrollbarWidth}}
onContextMenu={event => this.props.onHeaderContextMenu && this.props.onHeaderContextMenu(event)}
>
{columns.map(column => {
if(column.width) {
return (
<div key={"th-" + column.name}
className={cssStyle.dynamicColumn + " " + (column.className || "")}
style={{width: (column.width * 100 / totalWidth) + "%"}}
>
{column.header()}
</div>
);
} else if(column.fixedWidth) {
return (
<div key={"th-" + column.name}
className={cssStyle.fixedColumn + " " + (column.className || "")}
style={{width: column.fixedWidth}}
>
{column.header()}
</div>
);
}
})}
</div>
<div
className={cssStyle.body + " " + (this.props.bodyClassName || " ")}
ref={this.refBody}
onContextMenu={e => this.props.onBodyContextMenu && this.props.onBodyContextMenu(e)}
>
<div ref={this.refHiddenHeader} style={{height: this.lastHeaderHeight}} className={cssStyle.row} />
{body}
</div>
</div>
);
}
componentDidUpdate(prevProps: Readonly<TableProperties>, prevState: Readonly<TableState>, snapshot?: any): void {
if(!this.refHiddenHeader.current || !this.refHeader.current || !this.refBody.current)
return;
setTimeout(() => {
this.lastHeaderHeight = this.refHeader.current.clientHeight;
this.lastScrollbarWidth = this.refBody.current.parentElement.clientWidth - this.refBody.current.clientWidth;
this.refHiddenHeader.current.style.height = this.lastHeaderHeight + "px";
this.refHeader.current.style.right = this.lastScrollbarWidth + "px";
}, 10);
}
}