import cx from 'classnames'
import React from 'react'
import { ExpansionCell, icons, InlineFlexCell } from '../../common-views'
import type { ArtColumn, PositionKeysMapType } from '../../interfaces'
import { internals } from '../../internals'
import { isLeafNode as standardIsLeafNode, mergeCellProps } from '../../utils'
import { TablePipeline } from '../pipeline'
export const treeMetaSymbol = Symbol('treeMetaSymbol')
export interface TreeModeFeatureOptions {
/** 非受控用法:默认展开的 keys */
defaultOpenKeys?: string[]
/** 受控用法:当前展开的 keys */
openKeys?: string[]
/** 受控用法:展开 keys 改变的回调 */
onChangeOpenKeys?(nextKeys: string[], key: string, action: 'expand' | 'collapse'): void
/** 自定义叶子节点的判定逻辑 */
isLeafNode?(node: any, nodeMeta: { depth: number; expanded: boolean; rowKey: string, childField?: string }): boolean
/** icon 的缩进值。一般为负数,此时 icon 将向左偏移,默认从 pipeline.ctx.indents 中获取 */
iconIndent?: number
/** icon 与右侧文本的距离,默认从 pipeline.ctx.indents 中获取 */
iconGap?: number
/** 每一级缩进产生的距离,默认从 pipeline.ctx.indents 中获取 */
indentSize?: number
/** 点击事件的响应区域 */
clickArea?: 'cell' | 'content' | 'icon'
/** 是否对触发展开/收拢的 click 事件调用 event.stopPropagation() */
stopClickEventPropagation?: boolean
/** 指定表格每一行元信息的记录字段 */
treeMetaKey?: string | symbol
/**
* 展开符号添加的位置
* 默认第一项
*/
positionKey?: string | number;
/**根据层级进行处理展开节点*/
positionKeysMap?: PositionKeysMapType
}
export function treeMode(opts: TreeModeFeatureOptions = {}) {
return function treeModeStep(pipeline: TablePipeline) {
const stateKey = 'treeMode'
const ctx = pipeline.ctx
const positionKeysMap = opts.positionKeysMap ?? {};
// 对这个数据进行处理成需要的格式
const fieldMaps = Array.from(new Set(Object.entries(positionKeysMap).map(([_, value]) => value.code)))
const primaryKey = pipeline.ensurePrimaryKey('treeMode') as string
if (typeof primaryKey !== 'string') {
throw new Error('treeMode 仅支持字符串作为 primaryKey')
}
const openKeys: string[] = opts.openKeys ?? pipeline.getStateAtKey(stateKey) ?? opts.defaultOpenKeys ?? []
const openKeySet = new Set(openKeys)
const onChangeOpenKeys: TreeModeFeatureOptions['onChangeOpenKeys'] = (nextKeys: string[], key, action) => {
opts.onChangeOpenKeys?.(nextKeys, key, action)
pipeline.setStateAtKey(stateKey, nextKeys, { key, action })
}
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')
}
}
const isLeafNode = opts.isLeafNode ?? standardIsLeafNode
const clickArea = opts.clickArea ?? 'cell'
const treeMetaKey = opts.treeMetaKey ?? treeMetaSymbol
const stopClickEventPropagation = Boolean(opts.stopClickEventPropagation)
// indents
const iconWidth = ctx.indents.iconWidth
const iconIndent = opts.iconIndent ?? ctx.indents.iconIndent
const iconGap = opts.iconGap ?? ctx.indents.iconGap
const indentSize = opts.indentSize ?? ctx.indents.indentSize
const positionKey = opts.positionKey;
return pipeline.mapDataSource(processDataSource).mapColumns(processColumns)
function processDataSource(input: any[]) {
const result: any[] = []
dfs(input, 0)
function dfs(nodes: any[], depth: number) {
if (nodes == null) {
return
}
for (const node of nodes) {
const rowKey = node[primaryKey]
const expanded = openKeySet.has(rowKey)
const item = positionKeysMap?.[depth];
const isLeaf = isLeafNode(node, { depth, expanded, rowKey, childField: item?.childField || "children" })
const treeMeta = { depth, isLeaf, expanded, rowKey }
result.push({ [treeMetaKey]: treeMeta, ...node })
if (!isLeaf && expanded) {
if (item && item?.childField) {
dfs(node[item.childField], depth + 1)
} else {
dfs(node.children, depth + 1)
}
}
}
}
return result
}
function processColumns(columns: ArtColumn[]) {
if (columns.length === 0) {
return columns
}
const newColumns = [...columns]
let [firstCol] = newColumns
let positionIndex = 0;
if (typeof positionKey === "string") {
const colItemIndex = newColumns.findIndex((ite) => ite.code === positionKey);
if (colItemIndex >= 0) {
positionIndex = colItemIndex
firstCol = newColumns[colItemIndex]
}
} else if (typeof positionKey === 'number') {
const colItem = newColumns[positionKey]
if (colItem) {
positionIndex = positionKey
firstCol = colItem
}
}
const render = (value: any, record: any, recordIndex: number, firstCol: ArtColumn) => {
const content = internals.safeRender(firstCol, record, recordIndex)
if (record[treeMetaKey] == null) {
// 没有 treeMeta 信息的话,就返回原先的渲染结果
return content
}
const { rowKey, depth, isLeaf, expanded } = record[treeMetaKey]
const indent = iconIndent + depth * indentSize
if (isLeaf) {
return (
{content}
)
}
const item = positionKeysMap?.[depth]
if (item) {
// 判断当前是否是渲染字段,如果是则进行渲染图标,否则不进行渲染
if (item.code !== firstCol.code) {
return (
{content}
)
}
}
const onClick = (e: React.MouseEvent) => {
if (stopClickEventPropagation) {
e.stopPropagation()
}
toggle(rowKey)
}
const expandCls = expanded ? 'expanded' : 'collapsed'
return (
{content}
)
}
const getCellProps = (value: any, record: any, rowIndex: number, firstCol: ArtColumn) => {
const prevProps = internals.safeGetCellProps(firstCol, record, rowIndex)
if (record[treeMetaKey] == null) {
// 没有 treeMeta 信息的话,就返回原先的 cellProps
return prevProps
}
const { isLeaf, rowKey, depth } = record[treeMetaKey]
if (isLeaf) {
return prevProps
}
const item = positionKeysMap?.[depth]
if (item) {
// 判断当前是否是渲染字段,如果是则进行渲染图标,否则不进行渲染
if (item.code !== firstCol.code) {
return prevProps
}
}
return mergeCellProps(prevProps, {
onClick(e) {
if (stopClickEventPropagation) {
e.stopPropagation()
}
toggle(rowKey)
},
style: { cursor: 'pointer' },
})
}
/**多个字段映射时使用*/
if (fieldMaps.length > 0) {
for (const field of fieldMaps) {
const colItemIndex = newColumns.findIndex((ite) => ite.code === field);
if (colItemIndex >= 0) {
const firstCol = newColumns[colItemIndex]
const newItem = {
...firstCol,
title: (
{internals.safeRenderHeader(firstCol)}
),
render: (value: any, record: any, recordIndex: number) => render(value, record, recordIndex, firstCol),
getCellProps: (value: any, record: any, rowIndex: number) => clickArea === 'cell' ? getCellProps(value, record, rowIndex, firstCol) : firstCol.getCellProps?.(value, record, rowIndex),
}
newColumns[colItemIndex] = { ...newItem }
}
}
} else {
const newItem = {
...firstCol,
title: (
{internals.safeRenderHeader(firstCol)}
),
render: (value: any, record: any, recordIndex: number) => render(value, record, recordIndex, firstCol),
getCellProps: (value: any, record: any, rowIndex: number) => clickArea === 'cell' ? getCellProps(value, record, rowIndex, firstCol) : firstCol.getCellProps?.(value, record, rowIndex),
}
newColumns[positionIndex] = { ...newItem }
}
return [...newColumns];
}
}
}