[]
const theadStyle: React.CSSProperties = {
height: `${TD_HEIGHT[size || "small"] * getGroupColumnsDepth()}px`,
...((getHeadStyle && getHeadStyle()) || {}),
}
/**
* 展开行和选择行时,thead和每行的第一列前都要加入一列
* 此列也会影响 sticky 的偏移位置
*/
const selectAndExpand = !!onSelectChange && !!onExpandChange
const theadPlaceholderVisible =
expandIconVisible && (!!onSelectChange || !!onExpandChange)
const thead = (
{theadPlaceholderVisible && (
{selectMultiple &&
!!onSelectChange &&
(() => {
const hasSelectedAll = this.hasSelected("all")
const hasSelectedOne = this.hasSelected("one")
return (
)
})()}
)}
{columns.map((col, index) => {
const {
align,
dataIndex,
filters,
fixed,
grow: colGrow,
onFilter,
onSort,
sortOrder,
title,
width,
} = col
const isGrow = colGrow !== undefined ? colGrow : grow
const resizedCol = resized.find((o) => o.dataIndex === dataIndex)
const clickable =
!!onSort ||
(sortOrder !== null && sortOrder !== undefined) ||
!!onFilter ||
!!filters
const sortableAndFilterable =
(!!onSort || (sortOrder !== null && sortOrder !== undefined)) &&
(!!onFilter || !!filters)
let minWidth = 0
if (typeof title === "string") {
minWidth =
Math.ceil(title.length > 4 ? title.length / 2 : title.length) *
13 +
16
if (clickable) {
minWidth += 20
}
}
let flexValue
if (typeof width === "number") {
flexValue = `${width} 0 auto`
} else if (typeof width === "string") {
if (width.length - width.lastIndexOf("px") === 2) {
flexValue = `${width.slice(0, -2)} 0 auto`
} else {
flexValue = `0 0 ${width}`
}
} else {
flexValue = `${Math.max(TD_MIN_WIDTH, minWidth)} 0 auto`
}
/**
* th left 值:取得 resize-observer 存储的该列的偏移量,加上如果有选择行、展开行时的 SELECT_TD_WIDTH —— 仅对 left 有效
* th right 值:取得 resize-observer 存储的该列的偏移量,加上如果内滚动时的滚动条宽度(14) —— 仅对 right 有效
*/
return (
{
this.handleThResize(
columns.length,
widthResize,
dataIndex,
index,
fixed
)
}}
disabled={!fixed}
key={`${dataIndex || index}_${columns.length}`}
>
o.dataIndex === dataIndex
)?.isLastLeft,
[`${prefix}-th_rightFirst`]: fixedColumnsInfos.find(
(o) => o.dataIndex === dataIndex
)?.isFirstRight,
})}
key={dataIndex ? `${dataIndex}_${index}` : index}
style={{
left: isFixedLeft(col)
? (fixedColumnsInfos.find((o) => o.dataIndex === dataIndex)
?.offset || 0) +
(theadPlaceholderVisible
? selectAndExpand
? SELECT_AND_EXPAND_TD_WIDTH
: SELECT_TD_WIDTH
: 0)
: undefined,
right: isFixedRight(col)
? (fixedColumnsInfos.find((o) => o.dataIndex === dataIndex)
?.offset || 0) +
(isMainTableOverflowY && height ? 14 : 0)
: undefined,
flex: resizedCol ? `${resizedCol.value} 0 auto` : flexValue,
maxWidth: isGrow
? undefined
: resizedCol
? resizedCol.value
: width || "",
textAlign: align || undefined,
width: resizedCol
? resizedCol.value
: width || Math.max(TD_MIN_WIDTH, minWidth),
}}
data-column={index}
data-column-key={dataIndex ? `${dataIndex}_${index}` : index}
>
{this.generateThCell(col, { index, columns })}
{col.children && (
{col.children.map(
(childCol: IColumnProps
, childColIndex: number) => (
{this.generateThCell(childCol)}
{childCol.children && (
{childCol.children.map(
(
grandCol: IColumnProps
,
grandColIndex: number
) => (
{this.generateThCell(grandCol)}
)
)}
)}
)
)}
)}
)
})}
)
const generateTrs = (
row: T,
rowIndex: number,
style?: React.CSSProperties
) => {
const { key } = row
const colArray: any = []
const selectPropsGetted = getSelectProps && getSelectProps(row, rowIndex)
const allSelectProps = {
checked: selectedRowKeys.includes(key),
className: `${prefix}-selectComponent`,
onChange: (checked: boolean) => this.handleSelect(key, checked),
onClick: (e: React.MouseEvent) => e.stopPropagation(),
...(selectPropsGetted || {}),
}
const selectProps = omit(allSelectProps, ["popoverProps"])
const selectContent = selectMultiple ? (
) : (
)
const expandContent = (
this.handleExpand(key)}
role="none"
>
)
let selectCell = onExpandChange ? expandContent : selectContent
if (selectAndExpand) {
selectCell = (
{selectContent}
{expandContent}
)
}
const generateTr = (columnsParam: any[], parentIndex?: number) => {
columnsParam.forEach((col, colIndexParam) => {
/**
* 如果前面 的 col 有 children,则要把 children.length 额外加到 colIndex
*/
let colIndex = colIndexParam
if (parentIndex) {
for (let i = 0; i < parentIndex; i += 1) {
const childrenCount = columns[i].children?.length || 1
if (childrenCount) {
colIndex += childrenCount
}
}
}
if (!col.children) {
colArray.push(this.generateTbodyCell(row, col, rowIndex, colIndex))
} else {
generateTr(col.children, colIndexParam)
}
})
}
generateTr(columns)
let tr = (
onRowMouseEnter(row, rowIndex, e)
: undefined
}
onMouseLeave={
onRowMouseLeave
? (e) => onRowMouseLeave(row, rowIndex, e)
: undefined
}
onClick={(e) => this.handleRowClick(row, rowIndex, e)}
onKeyDown={noop}
style={{
...((getRowStyle && getRowStyle(row, rowIndex)) || {}),
...(style || {}),
}}
{...omit((getRowProps && getRowProps(row, rowIndex)) || {}, [
"popover",
])}
>
{theadPlaceholderVisible && (
{selectPropsGetted?.popoverProps?.popup ? (
{selectCell}
) : (
selectCell
)}
)}
{colArray}
)
if (getRowProps) {
const { popover } = getRowProps(row, rowIndex)
if (popover?.popup) {
tr = {tr}
}
}
if (onExpandChange) {
return (
{tr}
{expandedRowKeys.includes(key) ? (
{!!expandedRowRender && expandedRowRender(row, rowIndex)}
) : null}
)
}
return tr
}
return [
headerAffixed ? (
this.wrapper}
getTarget={headerAffixGetTarget}
width={mainTableStyle.width}
onChange={(bool) => {
this.setState({ affixedTop: bool }, () => {
// 固定的那一刻也需要立即重新定位。
// 重新定位、包括下面的 className, style 只在主表格上发生,因为固定列的 Affix 行为很单纯。
if (bool) {
this.handleWindowResize()
this.handleMainTableScroll()
}
})
}}
className={affixedTop ? `${prefix}-affix` : ""}
style={{
display: "flex",
zIndex: 2,
alignItems: "baseline",
flexDirection: "column",
...(affixedTop ? mainTableStyle : {}),
}}
ref={this.saveRef("affixHeader")}
>
{thead}
) : (
thead
),
{!virtualScroll &&
dataSource &&
!!dataSource.length &&
dataSource.map((row, rowIndex) => {
return generateTrs(row, rowIndex)
})}
{virtualScroll && dataSource && !!dataSource.length && (
42}
className={`${prefix}-virtual-wrapper`}
style={{
...virtualListStyle,
}}
ref={(o: any) => {
this.virtualList = o
}}
{...virtualListProps}
onScroll={(options: {
scrollDirection: "forward" | "backward"
scrollOffset: number
}) => {
if (virtualListProps?.onScroll) {
virtualListProps.onScroll(options)
}
}}
>
{({
index,
style,
}: {
index: number
style: React.CSSProperties
}) => generateTrs(dataSource[index], index, style)}
)}
,
isMainTableOverflowX && headerAffixed && (
this.mainTableBody}
getTarget={headerAffixGetTarget}
// @ts-ignore
ref={this.saveRef("affixScrollbar")}
className={classNames(`${prefix}-affixScrollbar`, {
[`${prefix}-affixScrollbar_show`]: affixedBottom,
})}
style={{
zIndex: 2,
...mainTableStyle,
}}
onScroll={(e: React.SyntheticEvent) =>
this.syncTableScrollPosition(e.currentTarget.scrollLeft)
}
onChange={(bool) => this.setState({ affixedBottom: bool })}
>
),
]
}
public generateThCell = (col: IColumnProps, options?: IBaseObject) => {
const {
align,
columnsResizable,
getHeadCellClassName: getHeadCellClassNameProp,
getHeadCellStyle: getHeadCellStyleProp,
} = this.props
const {
resizable: colResizable,
getHeadCellClassName,
getHeadCellStyle,
onSort,
sortOrder,
onFilter,
filters,
} = col
const clickable =
!!onSort ||
(sortOrder !== null && sortOrder !== undefined) ||
!!onFilter ||
!!filters
const sortableAndFilterable =
(!!onSort || (sortOrder !== null && sortOrder !== undefined)) &&
(!!onFilter || !!filters)
let resizable = true
if (typeof colResizable === "boolean") {
resizable = colResizable
} else if (typeof columnsResizable === "boolean") {
resizable = columnsResizable
}
const { currentlyResizing, isResizerShow } = this.state
const isLastTh = options && options.index === options.columns.length - 1
const thCell = (
{(!clickable || sortableAndFilterable) && col.title}
{(!!col.onSort ||
(col.sortOrder !== null && col.sortOrder !== undefined)) && (
(col.onSort ? col.onSort(order) : {})}
sortOrder={col.sortOrder}
title={col.title}
/>
)}
{(!!col.onFilter || !!col.filters) && (
{
if (col.onFilter) {
col.onFilter(filter)
}
}}
onFilterVisibleChange={(visible) => {
if (col.onFilterVisibleChange) {
col.onFilterVisibleChange(visible)
}
}}
title={col.title}
/>
)}
{resizable && !isLastTh && (
this.resizeColumnStart(e, col)}
role="none"
/>
)}
)
if (col.popover) {
return (
{thCell}
)
}
return thCell
}
public generateTbodyCell = (
row: T,
cell: IColumnProps,
rowIndex: number,
cellIndex: number
) => {
const {
align: alignProp,
expandIconVisible,
getCellClassName: getCellClassNameProp,
getCellProps: getCellPropsProp,
getCellStyle: getCellStyleProp,
grow,
onExpandChange,
onSelectChange,
verticalAlign: verticalAlignProp,
} = this.props
const selectAndExpand = !!onSelectChange && !!onExpandChange
const theadPlaceholderVisible =
expandIconVisible && (!!onSelectChange || !!onExpandChange)
const { currentlyResizing, fixedColumnsInfos, resized } = this.state
const {
align,
dataIndex,
filters,
grow: colGrow,
onFilter,
onSort,
render,
sortOrder,
title,
verticalAlign,
width,
getCellClassName,
getCellProps,
getCellStyle,
} = cell
const isGrow = colGrow !== undefined ? colGrow : grow
const clickable =
!!onSort ||
(sortOrder !== null && sortOrder !== undefined) ||
!!onFilter ||
!!filters
let minWidth = 0
if (typeof title === "string") {
minWidth =
Math.ceil(title.length > 4 ? title.length / 2 : title.length) * 13 + 16
if (clickable) {
minWidth += 20
}
}
let flexValue
if (typeof width === "number") {
flexValue = `${width} 0 auto`
} else if (typeof width === "string") {
if (width.length - width.lastIndexOf("px") === 2) {
flexValue = `${width.slice(0, -2)} 0 auto`
} else {
flexValue = `0 0 ${width}`
}
} else {
flexValue = `${Math.max(TD_MIN_WIDTH, minWidth)} 0 auto`
}
const resizedCol = resized.find((o) => o.dataIndex === dataIndex)
const cellClassName =
(getCellClassName && getCellClassName(row, rowIndex)) ||
(getCellClassNameProp &&
getCellClassNameProp(row, cell, rowIndex, cellIndex))
const cellProps =
(getCellProps && getCellProps(row, rowIndex)) ||
(getCellPropsProp && getCellPropsProp(row, cell, rowIndex, cellIndex)) ||
{}
const cellStyle =
(getCellStyle && getCellStyle(row, rowIndex)) ||
(getCellStyleProp && getCellStyleProp(row, cell, rowIndex, cellIndex)) ||
{}
const { rowSpan, colSpan, ...otherCellProps } = cellProps
if (rowSpan || colSpan) {
this.combinedCellsInfo.push({
cell,
cellIndex,
colSpan,
row,
rowIndex,
rowSpan,
})
}
return (
o.dataIndex === dataIndex
)?.isLastLeft,
[`${prefix}-td_rightFirst`]: fixedColumnsInfos.find(
(o) => o.dataIndex === dataIndex
)?.isFirstRight,
[`${prefix}-td_combined`]: rowSpan || colSpan,
})}
key={dataIndex ? `${dataIndex}_${cellIndex}` : cellIndex}
style={{
left: isFixedLeft(cell)
? (fixedColumnsInfos.find((o) => o.dataIndex === dataIndex)
?.offset || 0) +
(theadPlaceholderVisible
? selectAndExpand
? SELECT_AND_EXPAND_TD_WIDTH
: SELECT_TD_WIDTH
: 0)
: undefined,
right: isFixedRight(cell)
? fixedColumnsInfos.find((o) => o.dataIndex === dataIndex)?.offset
: undefined,
flex: resizedCol ? `${resizedCol.value} 0 auto` : flexValue,
width: resizedCol
? resizedCol.value
: width || Math.max(TD_MIN_WIDTH, minWidth),
maxWidth: isGrow
? undefined
: resizedCol
? resizedCol.value
: width || "",
}}
role="cell"
data-row={rowIndex}
data-column={cellIndex}
{...otherCellProps}
>
{render ? render(row, cell, rowIndex, cellIndex) : row[dataIndex]}
{(rowSpan || colSpan) && (
{render
? render(row, cell, rowIndex, cellIndex)
: row[dataIndex]}
)}
)
}
public getCombinedCellStyle = (
_: IBaseObject,
__: IColumnProps,
rowIndex: number,
colIndex: number,
rowSpan: number,
colSpan: number
) => {
const getTableCell = (rowIndexParam: number, colIndexParam: number) => {
if (this.mainTable) {
const row =
this.mainTable.querySelectorAll('[role="row"]')[rowIndexParam]
if (row) {
return row.children[colIndexParam]
}
}
return null
}
const cell = getTableCell(rowIndex, colIndex)
const style: React.CSSProperties = {}
if (rowSpan < 2 || typeof rowSpan === "undefined") {
// 最后 - 1 是为了不挡住 cell 的 box-shadow
style.height = "calc(100% - 1px)"
} else {
const endCell = getTableCell(rowIndex + rowSpan - 1, colIndex)
if (cell && endCell) {
const cellRect = cell.getBoundingClientRect()
const endCellRect = endCell.getBoundingClientRect()
// 最后 - 1 是为了不挡住 cell 的 box-shadow
style.height = `${
endCellRect.height + endCellRect.top - cellRect.top - 1
}px`
}
}
if (colSpan < 2 || typeof colSpan === "undefined") {
// 最后 - 1 是为了不挡住 cell 的 box-shadow
style.width = "calc(100% - 1px)"
} else {
const endCell = getTableCell(rowIndex, colIndex + colSpan - 1)
if (cell && endCell) {
const cellRect = cell.getBoundingClientRect()
const endCellRect = endCell.getBoundingClientRect()
// 最后 - 1 是为了不挡住 cell 的 box-shadow
style.width = `${
endCellRect.width + endCellRect.left - cellRect.left - 1
}px`
}
}
return style
}
public saveRef =
(
name?:
| "affixHeader"
| "mainTable"
| "mainTableBody"
| "mainThead"
| "wrapper"
) =>
(node: any) => {
if (name) {
this[name] = node
}
}
public render() {
const {
className,
dataSource,
emptyText,
headerAffixed,
headerAffixedOffsetTop,
headerAffixGetTarget,
headerEmphasized,
height,
loading,
size,
virtualScroll,
...otherProps
} = this.props
const restProps = omit(otherProps, [
"align",
"children",
"columns",
"columnsResizable",
"defaultExpandedRowKeys",
"defaultSelectedRowKeys",
"expandIconVisible",
"expandOnRowClick",
"expandedRowKeys",
"expandedRowRender",
"getCellClassName",
"getCellProps",
"getCellStyle",
"getHeadClassName",
"getHeadStyle",
"getHeadCellClassName",
"getHeadCellStyle",
"getRowClassName",
"getRowProps",
"getRowStyle",
"getSelectProps",
"grow",
"onExpandChange",
"onSelectChange",
"onScroll",
"onRowClick",
"onRowMouseEnter",
"onRowMouseLeave",
"selectMultiple",
"selectedRowKeys",
"selectOnRowClick",
"scrollXAffixedOffsetBottom",
"verticalAlign",
"virtualListProps",
"virtualListStyle",
])
const { children, isMainTableOverflowX, isMainTableOverflowY } = this.state
if (children) {
this.columnManager.reset(children)
}
const { getGroupColumnsDepth } = this.columnManager
const empty = !(dataSource && dataSource.length)
const classSet = classNames(
className,
`${prefix}-wrapper`,
`${prefix}-${size}`,
{
[`${prefix}-headerEmphasized`]: headerEmphasized,
[`${prefix}-empty`]: empty,
[`${prefix}-loading`]: loading,
[`${prefix}-overflow-x`]: isMainTableOverflowX,
[`${prefix}-overflow-y`]: isMainTableOverflowY,
[`${prefix}-virtualScroll`]: virtualScroll,
}
)
return (
{loading &&
(headerAffixed ? (
this.wrapper}
getTarget={headerAffixGetTarget}
className={`${prefix}-progress`}
>
) : (
))}
{this.generateTable()}
{empty && (
{emptyText}
)}
)
}
}
export default Table