/** * @file Tree * @description 树形组件 * @author fex */ import React from 'react'; import { eachTree, isVisible, autobind, findTreeIndex, hasAbility, createObject, getTreeParent } from '../utils/helper'; import {Option, Options, value2array} from './Select'; import {ClassNamesFn, themeable, ThemeProps} from '../theme'; import {highlight} from '../renderers/Form/Options'; import {Icon} from './icons'; import Checkbox from './Checkbox'; import {LocaleProps, localeable} from '../locale'; import Spinner from './Spinner'; interface TreeSelectorProps extends ThemeProps, LocaleProps { highlightTxt?: string; showIcon?: boolean; // 是否默认都展开 initiallyOpen?: boolean; // 默认展开的级数,从1开始,只有initiallyOpen不是true时生效 unfoldedLevel?: number; // 单选时,是否展示radio showRadio?: boolean; multiple?: boolean; // 是否都不可用 disabled?: boolean; // 多选时,选中父节点时,是否将其所有子节点也融合到取值中,默认是不融合 withChildren?: boolean; // 多选时,选中父节点时,是否只将起子节点加入到值中。 onlyChildren?: boolean; // 名称、取值等字段名映射 labelField: string; valueField: string; iconField: string; unfoldedField: string; foldedField: string; disabledField: string; // 是否显示 outline 辅助线 showOutline?: boolean; className?: string; itemClassName?: string; joinValues?: boolean; extractValue?: boolean; delimiter?: string; options: Options; value: any; onChange: Function; placeholder?: string; hideRoot?: boolean; rootLabel?: string; rootValue?: any; // 这个配置名字没取好,目前的含义是,如果这个配置成true,点父级的时候,子级点不会自选中。 // 否则点击父级,子节点选中。 cascade?: boolean; selfDisabledAffectChildren?: boolean; minLength?: number; maxLength?: number; // 是否为内建 增、改、删。当有复杂表单的时候直接抛出去让外层能统一处理 bultinCUD?: boolean; rootCreatable?: boolean; rootCreateTip?: string; creatable?: boolean; createTip?: string; onAdd?: ( idx?: number | Array, value?: any, skipForm?: boolean ) => void; editable?: boolean; editTip?: string; onEdit?: (value: Option, origin?: Option, skipForm?: boolean) => void; removable?: boolean; removeTip?: string; onDelete?: (value: Option) => void; onDeferLoad?: (option: Option) => void; } interface TreeSelectorState { value: Array; inputValue: string; addingParent: Option | null; isAdding: boolean; isEditing: boolean; editingItem: Option | null; } export class TreeSelector extends React.Component< TreeSelectorProps, TreeSelectorState > { static defaultProps = { showIcon: true, showOutline: false, initiallyOpen: true, unfoldedLevel: 0, showRadio: false, multiple: false, disabled: false, withChildren: false, onlyChildren: false, labelField: 'label', valueField: 'value', iconField: 'icon', unfoldedField: 'unfolded', foldedField: 'foled', disabledField: 'disabled', joinValues: true, extractValue: false, delimiter: ',', hideRoot: true, rootLabel: 'Tree.root', rootValue: 0, cascade: false, selfDisabledAffectChildren: true, rootCreateTip: 'Tree.addRoot', createTip: 'Tree.addChild', editTip: 'Tree.editNode', removeTip: 'Tree.removeNode' }; unfolded: WeakMap = new WeakMap(); constructor(props: TreeSelectorProps) { super(props); this.state = { value: value2array(props.value, { multiple: props.multiple, delimiter: props.delimiter, valueField: props.valueField, options: props.options }), inputValue: '', addingParent: null, isAdding: false, isEditing: false, editingItem: null }; this.syncUnFolded(props); } componentDidUpdate(prevProps: TreeSelectorProps) { const props = this.props; if (prevProps.options !== props.options) { this.syncUnFolded(props); } if ( prevProps.value !== props.value || prevProps.options !== props.options ) { this.setState({ value: value2array(props.value, { multiple: props.multiple, delimiter: props.delimiter, valueField: props.valueField, options: props.options }) }); } } syncUnFolded(props: TreeSelectorProps) { // 初始化树节点的展开状态 let unfolded = this.unfolded; const {foldedField, unfoldedField} = this.props; eachTree(props.options, (node: Option, index, level) => { if (unfolded.has(node)) { return; } if (node.children && node.children.length) { let ret: any = true; if (node.defer && node.loaded) { ret = true; } else if ( unfoldedField && typeof node[unfoldedField] !== 'undefined' ) { ret = !!node[unfoldedField]; } else if (foldedField && typeof node[foldedField] !== 'undefined') { ret = !node[foldedField]; } else { ret = !!props.initiallyOpen; if (!ret && level <= (props.unfoldedLevel as number)) { ret = true; } } unfolded.set(node, ret); } }); return unfolded; } @autobind toggleUnfolded(node: any) { const unfolded = this.unfolded; const {onDeferLoad} = this.props; if (node.defer && !node.loaded) { onDeferLoad?.(node); return; } unfolded.set(node, !unfolded.get(node)); this.forceUpdate(); } isUnfolded(node: any) { const unfolded = this.unfolded; return unfolded.get(node); } @autobind clearSelect() { this.setState( { value: [] }, () => { const {joinValues, rootValue, onChange} = this.props; onChange(joinValues ? rootValue : []); } ); } @autobind handleSelect(node: any, value?: any) { const {joinValues, valueField, onChange} = this.props; if (node[valueField as string] === undefined) { if (node.defer && !node.loaded) { this.toggleUnfolded(node); } return; } this.setState( { value: [node] }, () => { onChange(joinValues ? node[valueField as string] : node); } ); } @autobind handleCheck(item: any, checked: boolean) { const props = this.props; const value = this.state.value.concat(); const idx = value.indexOf(item); const onlyChildren = props.onlyChildren; if (checked) { ~idx || value.push(item); // cascade 为 true 表示父节点跟子节点没有级联关系。 if (!props.cascade) { const children = item.children ? item.children.concat([]) : []; if (onlyChildren) { // 父级选中的时候,子节点也都选中,但是自己不选中 !~idx && children.length && value.pop(); while (children.length) { let child = children.shift(); let index = value.indexOf(child); if (child.children) { children.push.apply(children, child.children); } else if (!~index && child.value !== 'undefined') { value.push(child); } } } else { // 只要父节点选择了,子节点就不需要了,全部去掉勾选. withChildren时相反 while (children.length) { let child = children.shift(); let index = value.indexOf(child); if (~index) { value.splice(index, 1); } if (props.withChildren) { value.push(child); } if (child.children && child.children.length) { children.push.apply(children, child.children); } } let toCheck = item; while (true) { const parent = getTreeParent(props.options, toCheck); if (parent?.value) { // 如果所有孩子节点都勾选了,应该自动勾选父级。 if ( parent.children.every((child: any) => ~value.indexOf(child)) ) { if (!props.withChildren) { parent.children.forEach((child: any) => { const index = value.indexOf(child); if (~index) { value.splice(index, 1); } }); } value.push(parent); toCheck = parent; continue; } } break; } } } } else { ~idx && value.splice(idx, 1); if (!props.cascade && (props.withChildren || onlyChildren)) { const children = item.children ? item.children.concat([]) : []; while (children.length) { let child = children.shift(); let index = value.indexOf(child); if (~index) { value.splice(index, 1); } if (child.children && child.children.length) { children.push.apply(children, child.children); } } } } this.setState( { value }, () => { const { joinValues, extractValue, valueField, delimiter, onChange } = props; onChange( joinValues ? value.map(item => item[valueField as string]).join(delimiter) : extractValue ? value.map(item => item[valueField as string]) : value ); } ); } @autobind handleAdd(parent: Option | null = null) { const {bultinCUD, onAdd, options} = this.props; if (!bultinCUD) { const idxes = findTreeIndex(options, item => item === parent) || []; return onAdd && onAdd(idxes.concat(0)); } else { this.setState({ isEditing: false, isAdding: true, addingParent: parent }); } } @autobind handleEdit(item: Option) { const {bultinCUD, onEdit, labelField, options} = this.props; if (!bultinCUD) { onEdit?.(item); } else { this.setState({ isEditing: true, isAdding: false, editingItem: item, inputValue: item[labelField] }); } } @autobind handleRemove(item: Option) { const {onDelete} = this.props; onDelete && onDelete(item); } @autobind handleInputChange(e: React.ChangeEvent) { this.setState({ inputValue: e.currentTarget.value }); } @autobind handleConfirm() { const { inputValue: value, isAdding, addingParent, editingItem, isEditing } = this.state; if (!value) { return; } const {labelField, onAdd, options, onEdit} = this.props; this.setState( { inputValue: '', isAdding: false, isEditing: false }, () => { if (isAdding && onAdd) { const idxes = (addingParent && findTreeIndex(options, item => item === addingParent)) || []; onAdd(idxes.concat(0), {[labelField]: value}, true); } else if (isEditing && onEdit) { onEdit( { ...editingItem, [labelField]: value }, editingItem!, true ); } } ); } @autobind handleCancel() { this.setState({ inputValue: '', isAdding: false, isEditing: false }); } renderInput(prfix: JSX.Element | null = null) { const {classnames: cx, translate: __} = this.props; const {inputValue} = this.state; return (
{prfix}
); } @autobind renderList( list: Options, value: Option[], uncheckable: boolean ): {dom: Array; childrenChecked: number} { const { itemClassName, showIcon, showRadio, multiple, disabled, labelField, valueField, iconField, disabledField, cascade, selfDisabledAffectChildren, onlyChildren, classnames: cx, highlightTxt, options, maxLength, minLength, creatable, editable, removable, createTip, editTip, removeTip, translate: __ } = this.props; const { value: stateValue, isAdding, addingParent, editingItem, isEditing } = this.state; let childrenChecked = 0; let ret = list.map((item, key) => { if (!isVisible(item as any, options)) { return null; } const checked = !!~value.indexOf(item); const selfDisabled = item[disabledField]; let selfChecked = !!uncheckable || checked; let childrenItems = null; let selfChildrenChecked = false; if (item.children && item.children.length) { childrenItems = this.renderList( item.children, value, cascade ? false : uncheckable || (selfDisabledAffectChildren ? selfDisabled : false) || (multiple && checked) ); selfChildrenChecked = !!childrenItems.childrenChecked; if ( !selfChecked && onlyChildren && item.children.length === childrenItems.childrenChecked ) { selfChecked = true; } childrenItems = childrenItems.dom; } if ((onlyChildren ? selfChecked : selfChildrenChecked) || checked) { childrenChecked++; } let nodeDisabled = !!uncheckable || !!disabled || selfDisabled; if ( !nodeDisabled && ((maxLength && !selfChecked && stateValue.length >= maxLength) || (minLength && selfChecked && stateValue.length <= minLength)) ) { nodeDisabled = true; } const checkbox: JSX.Element | null = multiple ? ( ) : showRadio ? ( ) : null; const isLeaf = (!item.children || !item.children.length) && !item.placeholder; return (
  • {isEditing && editingItem === item ? ( this.renderInput(checkbox) ) : (
    {item.loading ? ( ) : !isLeaf || (item.defer && !item.loaded) ? (
    this.toggleUnfolded(item)} className={cx('Tree-itemArrow', { 'is-folded': !this.isUnfolded(item) })} >
    ) : ( )} {checkbox} {showIcon ? ( !nodeDisabled && (multiple ? this.handleCheck(item, !selfChecked) : this.handleSelect(item)) } > ) : null} !nodeDisabled && (multiple ? this.handleCheck(item, !selfChecked) : this.handleSelect(item)) } > {highlightTxt ? highlight(`${item[labelField]}`, highlightTxt) : `${item[labelField]}`} {!nodeDisabled && !isAdding && !isEditing && !(item.defer && !item.loaded) ? (
    {creatable && hasAbility(item, 'creatable') ? ( ) : null} {removable && hasAbility(item, 'removable') ? ( ) : null} {editable && hasAbility(item, 'editable') ? ( ) : null}
    ) : null}
    )} {/* 有children而且为展开状态 或者 添加child时 */} {(childrenItems && this.isUnfolded(item)) || (isAdding && addingParent === item) ? (
      {isAdding && addingParent === item ? (
    • {this.renderInput( checkbox ? React.cloneElement(checkbox, { checked: false, disabled: true }) : null )}
    • ) : null} {childrenItems}
    ) : !childrenItems && item.placeholder && this.isUnfolded(item) ? (
    • {item.placeholder}
    ) : null}
  • ); }); return { dom: ret, childrenChecked }; } render() { const { className, placeholder, hideRoot, rootLabel, showOutline, showIcon, classnames: cx, creatable, rootCreatable, rootCreateTip, disabled, translate: __ } = this.props; let options = this.props.options; const {value, isAdding, addingParent, isEditing, inputValue} = this.state; let addBtn = null; if (creatable && rootCreatable !== false && hideRoot) { addBtn = ( {__(rootCreateTip)} ); } return (
    {(options && options.length) || addBtn || hideRoot === false ? (
      {hideRoot ? ( <> {addBtn} {isAdding && !addingParent ? (
    • {this.renderInput()}
    • ) : null} {this.renderList(options, value, false).dom} ) : (
    • {showIcon ? ( ) : null} {rootLabel} {!disabled && creatable && rootCreatable !== false && !isAdding && !isEditing ? (
      {creatable ? ( ) : null}
      ) : null}
        {isAdding && !addingParent ? (
      • {this.renderInput()}
      • ) : null} {this.renderList(options, value, false).dom}
    • )}
    ) : (
    {placeholder}
    )}
    ); } } export default themeable(localeable(TreeSelector));