import React, { forwardRef, useContext } from "react"; import warning from "warning"; import classNames from "classnames"; import { TableAddon, TableProps, RowRenderContext } from "../TableProps"; import { ChangeContext, ControlledProps } from "../../form/controlled"; import { CheckTreeRelation, CheckTree, completeDisabledNames, } from "../../checktree"; import { Checkbox } from "../../checkbox"; import { getRowKeyFromRecordKey } from "../util/get-row-key-from-record-key"; import { CheckContext } from "../../check"; import { injectPropsIfTargetNotExisted } from "../util/inject-props-if-target-not-existed"; import { useConfig } from "../../_util/config-context"; export interface SelectableChangeContext extends ChangeContext { /** * 当前被改变项 Key * * **多层展开表格下子行只能获取到 `recordKey`, 无法获取行数据** * * @since 2.7.2 */ recordKey: Record; /** * 当前被改变项是否选中 * @since 2.7.2 */ checked: boolean; /** * 当前被改变项数据 */ record: Record; /** * 被改变数据 */ selectedRecords: Record[]; /** * 发生变化的选项的更多信息 */ check: { name?: string; value?: boolean; all?: boolean }; } /** * `selectable` 插件用于支持表格可选择行的样式及操作。 */ export interface SelectableOptions extends ControlledProps< string[], React.SyntheticEvent, SelectableChangeContext > { /** * 不支持非受控模式 */ defaultValue?: never; /** * 提供 `relations` 属性,则会按照树状选择逻辑进行 */ relations?: CheckTreeRelation; /** * 提供 `all` 属性来支持全选 * 给定字符串可以用作全选项的 `key` */ all?: boolean | string; /** * 如果开启了全选支持,则可以指定哪些记录从全选的范围内排除 * * - 默认为 `disabled` 可以排除禁用的记录 * - 提供回调则自定义哪些记录应该排除,对于应该排除的记录,应该返回 `true` * * @default "disabled" */ shouldRecordExcludeFromAll?: "disabled" | ((record: any) => boolean); /** * 提供一个列的 `key`,将选择组件插入到一个目标列 * * 默认在最前新建一列插入 */ targetColumnKey?: string; /** * **\[Deprecated\]** 请使用 `indentable` 插件代替 * * @deprecated */ indent?: number; /** * 是否整行可选 */ rowSelect?: boolean; /** * 列宽度,可以指定 CSS 属性或数字 (单位:px) * @default 26 */ width?: string | number; /** * **高级用法** * 更改该插件的在每行的渲染内容,`element` 为默认渲染内容,`context` 中包含该行数据相关信息 * @default x => x */ render?: ( element: JSX.Element, context: RowRenderContext ) => React.ReactNode; /** * 指定行的选择是否可用 * * - 使用 Table 中的 `rowDisabled` 可将整行置灰并禁用选择 * * - `2.5.0` 新增 `record` 返回,但展开行无法获取该值 * * @default () => true */ rowSelectable?: ( rowKey: string, context: { parent: string; children: string[]; record?: Record } ) => boolean; } const fallbackColumnKey = "__selectable_addon__"; const fallbackAllKey = "__selectable_all__"; export function selectable(options: SelectableOptions): TableAddon { const { relations = {}, value, onChange, all = true, targetColumnKey, indent, rowSelect, width = 26, render = x => x, shouldRecordExcludeFromAll = "disabled", rowSelectable = () => true, } = options; if (typeof indent !== "undefined") { warning( false, "Property `indent` is deprecated. Please use `indentable` addon instead." ); } const allKey = typeof all === "string" ? all : fallbackAllKey; let rowDisabled: TableProps["rowDisabled"] = null; const childrenMap = new Map>(); for (const [child, parent] of Object.entries(relations)) { let childrenSet = childrenMap.get(parent); if (!childrenSet) { childrenSet = new Set(); childrenMap.set(parent, childrenSet); } childrenSet.add(child); } const relationsKeys = Object.keys(relations); return { getInfo: () => ({ name: "selectable" }), onInjectProps: props => { let { columns } = props; rowDisabled = props.rowDisabled || (() => false); columns = injectPropsIfTargetNotExisted(columns, targetColumnKey, { key: fallbackColumnKey, width, header: null, render: () => null, }); return { ...props, columns }; }, onInjectColumn: previous => ( record, rowKey, recordIndex, column, columnIndex ) => { // 不是目标列 if (column.key !== targetColumnKey && column.key !== fallbackColumnKey) { return previous(record, rowKey, recordIndex, column, columnIndex); } const { children: preChildren, props, ...result } = previous( record, rowKey, recordIndex, column, columnIndex ); let children = preChildren; // 表头 if (recordIndex === -1) { children = ( <> {children || <> } {!all ? children : null} ); } // 记录行 else { let paddingLeft = 0; let depth = 0; if (indent > 0) { let node = rowKey; while (relations && relations[node]) { depth += 1; node = relations[node]; } paddingLeft = indent * depth; } const checkbox = ( {children || <> } ); const element = paddingLeft ? (
{checkbox}
) : ( checkbox ); children = render(element, { children: preChildren, record, depth, rowKey, recordIndex, disabled: rowDisabled(record), }); } return { ...result, props, children }; }, onInjectTable: renderBody => props => { const { recordKey, records, rowDisabled = () => false } = props; const rowKey = getRowKeyFromRecordKey(recordKey); // 哪些应该从 all 里面排除 let shouldRecordExcludeFromAllFinal: (record: any) => boolean = null; if (typeof shouldRecordExcludeFromAll === "function") { shouldRecordExcludeFromAllFinal = shouldRecordExcludeFromAll; } else if (shouldRecordExcludeFromAll === "disabled") { shouldRecordExcludeFromAllFinal = rowDisabled; } shouldRecordExcludeFromAllFinal = shouldRecordExcludeFromAllFinal || (() => false); const recordRowKeys: string[] = []; const recordMap = new Map(); records.forEach((record, index) => { const key = rowKey(record, index); recordRowKeys.push(key); recordMap.set(key, record); }); const disabledKeys = Array.from([ ...relationsKeys, ...recordRowKeys, ]).filter( key => !rowSelectable(key, { parent: relations[key], children: childrenMap.has(relations[key]) ? [...childrenMap.get(relations[key])] : [], record: recordMap.get(key), }) ); const disabledKeySet = new Set(disabledKeys); // all keys const keys = Array.from( new Set([ ...relationsKeys, // 这里 records 不包括扩展行 ...records .filter(x => !shouldRecordExcludeFromAllFinal(x)) .map(rowKey) .filter(x => shouldRecordExcludeFromAll === "disabled" ? !disabledKeySet.has(x) : true ), ]) ); // keys without children const rootKeys = relations ? keys.filter(key => !relations[key]) : keys; const relationWithLeafs = { ...(relations || {}), }; for (const key of rootKeys) { relationWithLeafs[key] = allKey; } // 只有全选 Check 可用时 const hasRelations = !!Object.keys(relationWithLeafs).length; const disabledNames = [ ...records.filter(x => rowDisabled(x)).map(rowKey), ...disabledKeys, ]; const disabledSet = completeDisabledNames(disabledNames, childrenMap); return ( { let newValue = value; // 处理连选 if (context.shiftSelect) { const { startName, endName } = context.shiftSelect; const section = new Set(value); let inSection; records.forEach((record, index) => { const key = rowKey(record, index); if (inSection && !disabledSet.has(key)) { section.add(key); } if (key === startName || key === endName) { inSection = !inSection; if (inSection && !disabledSet.has(key)) { section.add(key); } } }); newValue = [...section]; } const newValueSet = new Set(newValue); const name = context.check.name || context.shiftSelect?.endName; onChange(newValue, { ...context, check: { all: context.check.name === allKey, ...context.check, }, checked: context.check.value, recordKey: name, record: records.find( (record, index) => name === rowKey(record, index) ), selectedRecords: records.filter((record, index) => newValueSet.has(rowKey(record, index)) ), }); } : null } disabledNames={disabledNames} > {renderBody(props)} ); }, onInjectRow: renderRow => (record, rowKey, recordIndex, columns) => { const { prepends, appends, row: preRow } = renderRow( record, rowKey, recordIndex, columns ); let row = preRow; // 支持整行选择 row = ( {row} ); return { prepends, row, appends }; }, }; } const SelectWrapper = forwardRef(function SelectWrapper( { name, rowSelect, children, ...props }: { name: string; rowSelect: boolean; children: React.ReactElement>; } & React.HTMLAttributes, ref ): JSX.Element { const context = useContext(CheckContext); if (context) { const checkProps = context.inject({ type: "checkbox", name }); const { onChange, value } = checkProps; const rowSelectProps = { onClick: (event: React.MouseEvent) => { // 事件合并 if (typeof props.onClick === "function") { props.onClick(event); } if (typeof children.props.onClick === "function") { children.props.onClick(event); } return onChange(!value, { event, check: { type: "checkbox", name, value: !value, all: false } as any, }); }, }; return React.cloneElement(children, { ...props, ref, className: classNames(props.className, children.props.className, { "is-selected": !!value, }), ...(rowSelect ? rowSelectProps : {}), }); } return children; }); function TableCheckbox({ children, ...props }) { const { classPrefix } = useConfig(); return ( {children} ); }