amis-rpc-design/libs/amis/packages/amis-ui/src/components/ResultTreeList.tsx

348 lines
8.5 KiB
TypeScript
Raw Normal View History

2023-10-07 19:42:30 +08:00
/**
* ()
*/
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import isEqual from 'lodash/isEqual';
import omit from 'lodash/omit';
import {Option, Options} from './Select';
import {ThemeProps, themeable} from 'amis-core';
import {autobind, noop} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import {BaseSelectionProps} from './Selection';
import Tree from './Tree';
import TransferSearch from './TransferSearch';
import {SpinnerExtraProps} from './Spinner';
export interface ResultTreeListProps
extends ThemeProps,
LocaleProps,
BaseSelectionProps,
SpinnerExtraProps {
className?: string;
title?: string;
searchable?: boolean;
value: Array<Option>;
valueField: string;
onSearch?: Function;
onChange: (value: Array<Option>, optionModified?: boolean) => void;
placeholder: string;
searchPlaceholder?: string;
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
itemClassName?: string;
cellRender?: (
column: {
name: string;
label: string;
[propName: string]: any;
},
option: Option,
colIndex: number,
rowIndex: number
) => JSX.Element;
}
export interface ItemRenderStates {
index: number;
disabled?: boolean;
onChange: (value: any, name: string) => void;
}
interface ResultTreeListState {
searching: Boolean;
treeOptions: Options;
searchTreeOptions: Options;
}
// 递归找到对应选中节点(dfs)
function getDeep(
node: Option,
cb: (node: Option) => boolean,
pathNodes: Array<Option>,
valueField: string
) {
if (node[valueField] && cb(node)) {
node.isChecked = true;
for (let i = pathNodes.length - 2; i >= 0; i--) {
if (!pathNodes[i].isChecked) {
pathNodes[i].isChecked = true;
continue;
}
break;
}
} else if (node.children && Array.isArray(node.children)) {
node.children.forEach(n => {
pathNodes.push(n);
getDeep(n, cb, pathNodes, valueField);
pathNodes.pop();
});
}
}
// 递归删除树多余的节点
function deepCheckedTreeNode(nodes: Array<Option>) {
let arr: Array<Option> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.isChecked) {
if (node.children && Array.isArray(node.children)) {
node.children = deepCheckedTreeNode(node.children);
}
arr.push(node);
}
}
return arr;
}
// 根据选项获取到结果
function getResultOptions(
value: Options = [],
options: Options,
valueField: string
) {
const newOptions = cloneDeep(options) as Array<Option>;
const callBack = (node: Option) =>
!!(value || []).find(target => target[valueField] === node[valueField]);
newOptions &&
newOptions.forEach(op => {
getDeep(op, callBack, [op], valueField);
});
return deepCheckedTreeNode(newOptions);
}
// 在包含回调函数情况下,遍历树
function deepTree(nodes: Options, cb: (node: Option) => void) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
cb(node);
if (node.children && Array.isArray(node.children)) {
deepTree(node.children, cb);
}
}
}
// 树的节点删除
function deepDeleteTree(nodes: Options, option: Option, valueField: string) {
let arr: Array<Option> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isEqual(node, option)) {
continue;
}
if (node.children && Array.isArray(node.children)) {
node.children = deepDeleteTree(node.children, option, valueField);
}
if (
(node.children && node.children.length > 0) ||
node[valueField] !== undefined
) {
arr.push(node);
}
}
return arr;
}
export class BaseResultTreeList extends React.Component<
ResultTreeListProps,
ResultTreeListState
> {
static itemRender(option: any) {
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
}
static defaultProps: Pick<ResultTreeListProps, 'placeholder' | 'itemRender'> =
{
placeholder: 'placeholder.selectData',
itemRender: this.itemRender
};
state: ResultTreeListState = {
searching: false,
treeOptions: [],
searchTreeOptions: []
};
searchRef?: any;
static getDerivedStateFromProps(props: ResultTreeListProps) {
const newOptions = getResultOptions(
props.value,
props.options,
props.valueField
);
return {
treeOptions: cloneDeep(newOptions)
};
}
@autobind
domSearchRef(ref: any) {
while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
this.searchRef = ref;
}
// 删除非选中节点
@autobind
deleteTreeChecked(option: Option) {
const {value = [], onChange, valueField} = this.props;
const {searching, treeOptions} = this.state;
let temNode: Options = [];
const cb = (node: Option) => {
// 对比时去掉 parent因为其无限嵌套
if (isEqual(omit(node, 'parent'), omit(option, 'parent'))) {
temNode = [node];
}
};
deepTree(treeOptions || [], cb);
let arr: Options = [];
const cb2 = (node: Option) => {
if (node.isChecked && node[valueField]) {
arr.push(node);
}
};
deepTree(temNode, cb2);
onChange &&
onChange(
value.filter(
item =>
!arr.find(arrItem =>
// 对比时去掉 parent因为其无限嵌套且不相等
isEqual(
omit(arrItem, ['isChecked', 'childrens', 'parent']),
omit(item, 'parent')
)
)
)
);
// 搜索时,重新生成树
searching && this.deleteResultTreeNode(option);
}
// 搜索树点击删除时,删除对应节点
deleteResultTreeNode(option: Option) {
const arr = deepDeleteTree(
cloneDeep(this.state.searchTreeOptions) || [],
option,
this.props.valueField
);
this.setState({searchTreeOptions: arr});
}
@autobind
search(inputValue: string) {
// 结果为空,直接清空
if (!inputValue) {
this.clearSearch();
return;
}
const {valueField, onSearch} = this.props;
let temOptions: Array<Option> = this.state.treeOptions || [];
const cb = (node: Option) => {
node.isChecked = false;
return true;
};
deepTree(temOptions, cb);
const callBack = (node: Option) => onSearch?.(inputValue, node);
temOptions &&
temOptions.forEach(op => {
getDeep(op, callBack, [op], valueField);
});
this.setState({
searching: true,
searchTreeOptions: deepCheckedTreeNode(temOptions)
});
}
@autobind
clearSearch() {
this.setState({
searching: false,
searchTreeOptions: []
});
}
@autobind
clearInput() {
if (this.props.searchable) {
this.searchRef?.clearInput?.();
}
this.clearSearch();
}
renderTree() {
const {
className,
classnames: cx,
value,
valueField,
itemRender,
translate: __,
placeholder,
virtualThreshold,
itemHeight,
loadingConfig
} = this.props;
const {treeOptions, searching, searchTreeOptions} = this.state;
return (
<div className={cx('ResultTreeList', className)}>
{Array.isArray(value) && value.length ? (
<Tree
className={cx('Transfer-tree')}
options={!searching ? treeOptions : searchTreeOptions}
valueField={valueField}
value={[]}
onChange={noop}
showIcon={false}
itemRender={itemRender}
removable
loadingConfig={loadingConfig}
onDelete={(option: Option) => this.deleteTreeChecked(option)}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : (
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>
)}
</div>
);
}
render() {
const {
classnames: cx,
className,
title,
searchable,
translate: __,
searchPlaceholder = __('Transfer.searchKeyword')
} = this.props;
return (
<div className={cx('Selections', className)}>
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
{searchable ? (
<TransferSearch
ref={this.domSearchRef}
placeholder={searchPlaceholder}
onSearch={this.search}
onCancelSearch={this.clearSearch}
/>
) : null}
{this.renderTree()}
</div>
);
}
}
export default themeable(localeable(BaseResultTreeList));