import cx from 'classnames' import React, { ReactNode } from 'react' import { ExpansionCell, icons, InlineFlexCell } from '../../common-views' import type { ArtColumn } from '../../interfaces' import { internals } from '../../internals' import { collectNodes, mergeCellProps } from '../../utils' import { always, flatMap } from '../../utils/others' import { TablePipeline } from '../pipeline' export interface RowDetailFeatureOptions { /** 非受控用法:是否默认展开所有详情单元格 */ defaultOpenAll?: boolean /** 非受控用法:默认展开的 keys */ defaultOpenKeys?: string[] /** 受控用法:当前展开的 keys */ openKeys?: string[] /** 受控用法:openKeys 改变的回调 */ onChangeOpenKeys?(nextKeys: string[], key: string, action: 'expand' | 'collapse'): void /** 详情单元格的渲染方法 */ renderDetail?(row: any, rowIndex: number): ReactNode /** 是否包含详情单元格 */ hasDetail?(row: any, rowIndex: number): ReactNode /** 获取详情单元格所在行的 key,默认为 `(row) => row[primaryKey] + '_detail'` */ getDetailKey?(row: any, rowIndex: number): string /** 详情单元格 td 的额外样式 */ detailCellStyle?: React.CSSProperties /** 点击事件的响应区域 */ clickArea?: 'cell' | 'content' | 'icon' /** 是否对触发展开/收拢的 click 事件调用 event.stopPropagation() */ stopClickEventPropagation?: boolean /** 指定表格每一行元信息的记录字段 */ rowDetailMetaKey?: string | symbol } const rowDetailSymbol = Symbol('row-detail') const fallbackRenderDetail = () => (
设置 rowDetail.renderDetail 来自定义详情内容
) export function rowDetail(opts: RowDetailFeatureOptions = {}) { return function rowDetailStep(pipeline: TablePipeline) { const stateKey = 'rowDetail' const primaryKey = pipeline.ensurePrimaryKey('rowDetail') as string if (typeof primaryKey !== 'string') { throw new Error('rowDetail 仅支持字符串作为 primaryKey') } const rowDetailMetaKey = opts.rowDetailMetaKey ?? rowDetailSymbol const indents = pipeline.ctx.indents const textOffset = indents.iconIndent + indents.iconWidth + indents.iconGap const clickArea = opts.clickArea ?? 'cell' const getDetailKey = opts.getDetailKey ?? ((row) => row[primaryKey] + '_detail') const renderDetail = opts.renderDetail ?? fallbackRenderDetail const hasDetail = opts.hasDetail ?? always(true) const openKeys: string[] = opts.openKeys ?? pipeline.getStateAtKey(stateKey) ?? (opts.defaultOpenAll ? pipeline .getDataSource() .filter(hasDetail) .map((row) => row[primaryKey]) : opts.defaultOpenKeys) ?? [] const onChangeOpenKeys: RowDetailFeatureOptions['onChangeOpenKeys'] = (nextKeys, key, action) => { opts.onChangeOpenKeys?.(nextKeys, key, action) pipeline.setStateAtKey(stateKey, nextKeys, { key, action }) } const openKeySet = new Set(openKeys) const toggle = (rowKey: string) => { const expanded = openKeySet.has(rowKey) if (expanded) { onChangeOpenKeys( openKeys.filter((key) => key !== rowKey), rowKey, 'collapse', ) } else { onChangeOpenKeys([...openKeys, rowKey], rowKey, 'expand') } } return pipeline .dataSource( flatMap(pipeline.getDataSource(), (row, rowIndex) => { if (openKeySet.has(row[primaryKey])) { return [row, { [rowDetailMetaKey]: true, ...row, [primaryKey]: getDetailKey(row, rowIndex) }] } else { return [row] } }), ) .columns(processColumns(pipeline.getColumns())) .appendRowPropsGetter((row) => { if (row[rowDetailMetaKey]) { return { className: 'no-hover' } } }) function processColumns(columns: ArtColumn[]) { if (columns.length === 0) { return columns } const columnFlatCount = collectNodes(columns, 'leaf-only').length const [firstCol, ...others] = columns const render = (value: any, row: any, rowIndex: number) => { if (row[rowDetailMetaKey]) { return renderDetail(row, rowIndex) } const content = internals.safeRender(firstCol, row, rowIndex) if (!hasDetail(row, rowIndex)) { return {content} } const rowKey = row[primaryKey] const expanded = openKeySet.has(rowKey) const onClick = (e: React.MouseEvent) => { if (opts.stopClickEventPropagation) { e.stopPropagation() } toggle(rowKey) } const expandCls = expanded ? 'expanded' : 'collapsed' return ( {content} ) } const getCellProps = (value: any, row: any, rowIndex: number) => { if (row[rowDetailMetaKey]) { return { style: { '--cell-padding': '0', overflow: 'hidden', ...opts.detailCellStyle, } as any, } } const prevProps = firstCol.getCellProps?.(value, row, rowIndex) if (!hasDetail(row, rowIndex)) { return prevProps } return mergeCellProps(prevProps, { onClick(e) { if (opts.stopClickEventPropagation) { e.stopPropagation() } toggle(row[primaryKey]) }, style: { cursor: 'pointer' }, }) } return [ { ...firstCol, title: (
{internals.safeRenderHeader(firstCol)}
), render, getCellProps: clickArea === 'cell' ? getCellProps : firstCol.getCellProps, getSpanRect(value: any, row: any, rowIndex: number) { if (row[rowDetailMetaKey]) { // detail 总是成一行 return { top: rowIndex, bottom: rowIndex + 1, left: 0, right: columnFlatCount } } }, }, ...others, ] } } }