alibaba/noform

View on GitHub
packages/next-components/src/components/Table.tsx

Summary

Maintainability
F
4 days
Test Coverage
import React, { forwardRef, useEffect, useContext } from 'react'
import { TableProvider, LoadingProvider, ListLifeCycleTypes, useToggle, ToggleContext, ListContext } from '@alist/react'
import { TableProps } from '@alifd/next/types/table'
import { IVirtualBoxProps, createVirtualBox, createControllerBox, FormExpressionScopeContext, complieExpression } from '@formily/next'
import { Table as NextTable, Loading } from '@alifd/next'
import { ColumnProps, ColumnGroupProps, GroupFooterProps, GroupHeaderProps } from '@alifd/next/types/table'
import moment from 'moment'
import styled, { css } from 'styled-components'
import { InternalSorter as Sorter } from './Sorter'

// 遵循最小可用原则来采集初始化的table属性
const pickInitialTableProps = (props) => {
    const result = {}
    const attrs = ['rowSelection', 'className', 'primaryKey']
    attrs.forEach(k => {
        if (k in props) {
            result[k] = props[k]
        }
    })
    return result
}

const VerCenterTitle = styled.div`
    display: flex;
    align-items: center;
    justify-content: ${props => {
        const alignMap = {
            left: 'flex-start',
            right: 'flex-end',
            center: 'center'
        }
        return alignMap[props.align]
    }};
`

const LoadingWrapper = styled((props) => {
    return <Loading {...props} />
})`
    .next-loading-wrap {
        height: 100%;
        z-index: 3;
    }
`

const RecursionTable = (props) => {
    const { dataSource,
        hasExpandedRowCtrl = true, expandedRowIndent,
        isLoop = false, loopProps = {},
        isRoot,
        loading,
        ...others
    } = props
    const hasExtraRow = (dataSource || []).find(item => Array.isArray(item.children) && item.children.length > 0)
    const { enableHookCrtl, openRowKeys, toggle, toggleAll, toggleState } = useToggle({...props, toggleeKey: 'openRowKeys' })
    const expandProps: any = {};
    
    const list: any = useContext(ListContext)
    
    const columnsProps = {}
    const { columns, children } = others 
    if (list) {
        // 第一次会进入这里
        if (list.getAllColumns().length === 0) {
            if (columns) {
                list.setAllColumns(columns)
                list.setColumns(columns, { init: true })
            } else {
                list.setAllColumns(children || [])
                list.setColumns(children || [], { init: true })
            }
        }

        if (list.hasSetColumns()) {
            columnsProps[columns ? 'columns' : 'children'] = list.getColumns()
        }
    }

    useEffect(() => {
        if (isRoot && list) {
            list.actions.addAPI('toggle', toggle)
            list.actions.addAPI('toggleAll', toggleAll)
            list.actions.addAPI('getToggleState', () => toggleState)

            const id = list.subscribe(ListLifeCycleTypes.ON_LIST_TOGGLE, (action) => {
                if (props.onRowOpen) {
                    const { openRowKeys, currentRowKey, expanded, currentRecord } = action.payload || {}
                    props.onRowOpen(openRowKeys, currentRowKey, expanded, currentRecord)
                }
            })

            return function cleanup() {
                list.unSubscribe(id)
            }
        }
    }, [list, props.onRowOpen])

    let defaultExpandedRowIndent = [1, 0]
    if (isLoop) {
        defaultExpandedRowIndent = [0, 0]
        if (hasExtraRow) {
            expandProps.expandedRowRender=(record) => <RecursionTable
                    hasHeader={false}
                    hasBorder
                    dataSource={record.children}
                    {...others}
                    {...loopProps}
                    isLoop
                />
        }
    }

    if (enableHookCrtl) {
        expandProps.openRowKeys = props.openRowKeys || openRowKeys
    }

    return <ToggleContext.Provider value={{ toggle, openRowKeys, toggleAll, toggleState }}>
        <div style={{ position: 'relative' }}>
            <LoadingProvider>
                {(loading) => {
                    return <LoadingWrapper visible={loading} style={{ width: '100%', position: 'absolute', top: 0, bottom: 0 }} >
                        <div style={{ height: '100%', background: '#fff', width: '100%' }} />
                    </LoadingWrapper>
                }}
            </LoadingProvider>
            <NextTable
                dataSource={dataSource}
                {...expandProps}
                {...others}
                {...columnsProps}
                hasExpandedRowCtrl={hasExpandedRowCtrl}
                expandedRowIndent={expandedRowIndent || defaultExpandedRowIndent}
            />
        </div>
    </ToggleContext.Provider>
}

const TableStyledWrapper = styled(forwardRef((props, ref) => {
    const { loopBackground, hasBorder, hasTreeCtrl, ...others }  = props
    return <div {...others} ref={ref} />
}))`
    margin-bottom: 16px;

    .alist-recursion-table {
        table {
            td.next-table-cell.first {
                ${props => (props.hasTreeCtrl === false) && css`
                    > div.next-table-cell-wrapper {
                        padding-left: 16px !important;

                        .next-table-tree-placeholder,
                        .next-table-tree-arrow {
                            display: none;
                        }
                    }
                `}
            }
            .next-table-expanded-row {        
                td {
                    border-width: ${(props) => ((props.hasBorder === undefined ? true : !!props.hasBorder) ? 1 : 0)}px;
                }
        
                & > td {
                    border-left-width: 0;
                    border-right-width: 0;
                }

                .next-table-row:last-child td {
                    border-bottom-width: 0;
                }

                & > td {
                    border-bottom-width: 0;
                }

                tr {
                    ${props => props.loopBackground && css`
                        background: ${props.loopBackground}
                    ` }
                }
            }
        }
    
        .next-table.no-header table {
            tr:first-child td {
                border-top-width: 0;
            }
        }   
    }

    & > .next-table > table > .next-table-body > .next-table-expanded-row:last-child > td{
        border-bottom-width: ${(props) => ((props.hasBorder === undefined ? true : !!props.hasBorder) ? 1 : 0)}px;
    }
`

type AListTableProps = TableProps & { loopBackground?: boolean, hasTreeCtrl?: boolean }
type InternalTableType =  React.FunctionComponent<AListTableProps> & {
    Column: typeof NextTable.Column
    ColumnGroup: typeof NextTable.ColumnGroup,
    GroupHeader: typeof NextTable.GroupHeader,
    GroupFooter: typeof NextTable.GroupFooter,
}

const needComputeColumnProps = (itemProps) => ['moment', 'sortable'].filter(k => (k in (itemProps || {}))).length > 0
const computeColumnProps = (itemProps, opts) => {
    const cloneProps = { ...(itemProps || {}) }
    const { onSort } = opts || {}
    const { title, dataIndex, alignHeader, align } = cloneProps
    if (itemProps.sortable) {
        cloneProps.sortable = undefined;
        cloneProps.title = <VerCenterTitle align={alignHeader || align || 'left'}>
            {title}
            <Sorter dataIndex={dataIndex} onSort={(dataIndex, order) => {
                onSort(dataIndex, order)
            }}/>
        </VerCenterTitle>
    }

    if (itemProps.moment) {
        cloneProps.cell = (val) => {
            const format = typeof itemProps.moment === 'string' ? itemProps.moment : 'YYYY-MM-DD HH:mm:ss';
            return val ? moment(isNaN(val) ? val : Number(val)).format(format) : null;
        }
    }

    return cloneProps
}

const noop = () => {}
const InternalTable: InternalTableType = props => {
    const { onSort = noop, onFilter = noop, ...others } = props
    const columns = React.Children.map(props.children, (item: any) => {
        if (!item) return item
        /* 处理嵌套类的情况 */
        if (item.props && Array.isArray(item.props.children)) {
          return {
            ...item,
            props: {
              ...item.props,
              children: item.props.children.map(subItem => {
                if (needComputeColumnProps(subItem.props)) {
                    const cloneProps = computeColumnProps(subItem.props, { onSort })
                    return React.cloneElement(subItem, cloneProps)
                } else {
                    return subItem
                }
              })
            }
          }          
        }
        if (needComputeColumnProps(item.props)) {
            const cloneProps = computeColumnProps(item.props, { onSort })
            return React.cloneElement(item, cloneProps)
        } else {
            return item
        }
    })

    return <TableStyledWrapper
        hasBorder={props.hasBorder}
        loopBackground={props.loopBackground}
        hasTreeCtrl={props.hasTreeCtrl}
    >
        <TableProvider pickInitialTableProps={pickInitialTableProps} {...others}>
            {(connectProps, list) => {
                const extraProps: any = {}
                if ('hasBorder' in connectProps) extraProps.hasBorder = connectProps.hasBorder

                return <RecursionTable
                    {...connectProps}
                    {...props}
                    {...extraProps}
                    children={columns}
                    isRoot
                    onFilter={(filterParams) => {
                        onFilter(filterParams)
                        list.notify(ListLifeCycleTypes.ON_LIST_FILTER, filterParams)
                    }}
                    className={`${connectProps.className || ''} ${props.className || ''} alist-recursion-table`}
                />
            }}
        </TableProvider>
    </TableStyledWrapper>
}

InternalTable.Column = NextTable.Column;
InternalTable.ColumnGroup = NextTable.ColumnGroup,
InternalTable.GroupHeader = NextTable.GroupHeader;
InternalTable.GroupFooter = NextTable.GroupFooter;

const ComponentMap = {
    'alist-table-column': InternalTable.Column,
    'alist-table-group-header': InternalTable.GroupHeader,
    'alist-table-group-footer': InternalTable.GroupFooter,
    'alist-table-column-group': InternalTable.ColumnGroup,
}


type ExtendsProps = {
    Column?: React.FC<IVirtualBoxProps<ColumnProps>>,
    ColumnGroup?: React.FunctionComponent<ColumnGroupProps>,
    GroupHeader?: React.FunctionComponent<GroupHeaderProps>,
    GroupFooter?: React.FunctionComponent<GroupFooterProps>,
}

const SchemaTable = (props) => {
    const { schema, children } = props
    const componentProps = schema.getExtendsComponentProps()
    const { columns, ...others } = componentProps
    if (columns) {
        return <InternalTable {...componentProps} />
    } else if (Array.isArray(children) && children.length) {
        const expressionScope = useContext(FormExpressionScopeContext)
        const tableColumns = children.map(child => {
            const targetComponent = child.props.schema.getExtendsComponent()
            const targetComponentProps = complieExpression(
                child.props.schema.getExtendsComponentProps(),
                expressionScope
            )
            
            if (targetComponent === 'alist-table-column-group') {
                return React.createElement(ComponentMap[targetComponent], {
                    ...targetComponentProps,
                    children: tableColumns(targetComponentProps.children)
                })
            } else {
                return React.createElement(ComponentMap[targetComponent], targetComponentProps)
            }
        })

        return <InternalTable {...others}>
            {tableColumns}
        </InternalTable>
    }
    return null
}


type VirtualTableProps = React.FC<IVirtualBoxProps<AListTableProps>> & ExtendsProps
const Table: VirtualTableProps = createControllerBox<AListTableProps>('alist-table', SchemaTable)

Table.Column = createVirtualBox<ColumnProps>('alist-table-column', InternalTable.Column)
Table.GroupHeader = createVirtualBox<GroupHeaderProps>('alist-table-group-header', InternalTable.GroupHeader)
Table.GroupFooter = createVirtualBox<GroupFooterProps>('alist-table-group-footer', InternalTable.GroupFooter)
Table.ColumnGroup = createVirtualBox<ColumnGroupProps>('alist-table-column-group', InternalTable.ColumnGroup)

export {
    InternalTable,
    Table,
    SchemaTable,
    VirtualTableProps,
    AListTableProps,
    InternalTableType,
}