amis-rpc-design/libs/amis/packages/amis-ui/src/components/Selection.tsx
2023-10-07 19:42:30 +08:00

288 lines
6.9 KiB
TypeScript

/**
* @file Checkboxes
* @description 多选输入框
* @author fex
*/
import React from 'react';
import isEqual from 'lodash/isEqual';
import cx from 'classnames';
import {
uncontrollable,
LocaleProps,
localeable,
themeable,
ThemeProps,
autobind,
findTree,
flattenTree,
getOptionValue,
getOptionValueBindField
} from 'amis-core';
import Checkbox from './Checkbox';
import {Option, Options} from './Select';
export interface BaseSelectionProps extends ThemeProps, LocaleProps {
options: Options;
className?: string;
placeholder?: string;
value?: any | Array<any>;
multiple?: boolean;
clearable?: boolean;
labelField?: string;
valueField?: string;
onChange?: (value: Array<any> | any) => void;
onDeferLoad?: (option: Option) => void;
onLeftDeferLoad?: (option: Option, leftOptions: Option) => void;
inline?: boolean;
labelClassName?: string;
option2value?: (option: Option) => any;
itemClassName?: string;
itemHeight?: number; // 每个选项的高度,主要用于虚拟渲染
virtualThreshold?: number; // 数据量多大的时候开启虚拟渲染
virtualListHeight?: number; // 虚拟渲染时,列表高度
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
disabled?: boolean;
onClick?: (e: React.MouseEvent) => void;
placeholderRender?: (props: any) => JSX.Element | null;
checkAll?: boolean;
checkAllLabel?: string;
}
export interface ItemRenderStates {
index: number;
labelField?: string;
multiple?: boolean;
checked: boolean;
onChange: () => void;
disabled?: boolean;
}
export class BaseSelection<
T extends BaseSelectionProps = BaseSelectionProps,
S = any
> extends React.Component<T, S> {
static itemRender(option: Option, states: ItemRenderStates) {
return (
<span className={cx({'is-invalid': option?.__unmatched})}>
{option[states?.labelField || 'label']}
{option.tip || ''}
</span>
);
}
static defaultProps = {
placeholder: 'placeholder.noOption',
itemRender: this.itemRender,
multiple: true,
clearable: false,
virtualThreshold: 1000,
itemHeight: 32
};
static value2array(
value: any,
options: Options,
option2value: (option: Option) => any = (option: Option) => option,
valueField?: string
): Options {
if (value === void 0) {
return [];
}
if (!Array.isArray(value)) {
value = [value];
}
return value.map((value: any) => {
const option = findTree(
options,
option => isEqual(option2value(option), value),
valueField
? {
value: getOptionValue(value, valueField),
resolve: getOptionValueBindField(valueField)
}
: undefined
);
return option || value;
});
}
static resolveSelected(
value: any,
options: Options,
option2value: (option: Option) => any = (option: Option) => option
) {
value = Array.isArray(value) ? value[0] : value;
return findTree(options, option => isEqual(option2value(option), value));
}
// 获取两个数组的交集
intersectArray(
arr1: undefined | Array<Option>,
arr2: undefined | Array<Option>
): Array<Option> {
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
return [];
}
const len1 = arr1.length;
const len2 = arr2.length;
if (len1 < len2) {
return this.intersectArray(arr2, arr1);
}
return Array.from(new Set(arr1.filter(item => arr2.includes(item))));
}
toggleOption(option: Option) {
const {
value,
onChange,
option2value,
options,
disabled,
multiple,
clearable,
valueField
} = this.props;
if (disabled || option.disabled) {
return;
}
let valueArray = BaseSelection.value2array(
value,
options,
option2value,
valueField
);
let idx = valueArray.indexOf(option);
if (~idx && (multiple || clearable)) {
valueArray.splice(idx, 1);
} else if (multiple) {
valueArray.push(option);
} else {
valueArray = [option];
}
let newValue: string | Array<Option> = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(multiple ? newValue : newValue[0]);
}
getAvailableOptions() {
const {options} = this.props;
const flattendOptions = flattenTree(options, item =>
item.children ? null : item
).filter(a => a && !a.disabled);
return flattendOptions as Option[];
}
@autobind
toggleAll() {
const {value, onChange, option2value, options} = this.props;
let valueArray: Array<Option> = [];
const availableOptions = this.getAvailableOptions();
const intersectOptions = this.intersectArray(value, availableOptions);
if (!Array.isArray(value)) {
valueArray = availableOptions;
} else if (intersectOptions.length < availableOptions.length) {
valueArray = Array.from(new Set([...value, ...availableOptions]));
} else {
valueArray = value.filter(
(item: Option) => !availableOptions.includes(item)
);
}
let newValue: string | Array<Option> = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
}
render() {
const {
value,
options,
className,
placeholder,
inline,
labelClassName,
disabled,
classnames: cx,
option2value,
itemClassName,
itemRender,
multiple,
labelField,
valueField,
onClick
} = this.props;
const __ = this.props.translate;
let valueArray = BaseSelection.value2array(
value,
options,
option2value,
valueField
);
let body: Array<React.ReactNode> = [];
if (Array.isArray(options) && options.length) {
body = options.map((option, key) => (
<Checkbox
type={multiple ? 'checkbox' : 'radio'}
className={cx(itemClassName, option.className)}
key={key}
onChange={() => this.toggleOption(option)}
checked={!!~valueArray.indexOf(option)}
disabled={disabled || option.disabled}
labelClassName={labelClassName}
description={option.description}
>
{itemRender(option, {
index: key,
multiple: multiple,
checked: !!~valueArray.indexOf(option),
onChange: () => this.toggleOption(option),
labelField,
disabled: disabled || option.disabled
})}
</Checkbox>
));
}
return (
<div
className={cx(
'Selection',
className,
inline ? 'Selection--inline' : ''
)}
onClick={onClick}
>
{body && body.length ? body : <div>{__(placeholder)}</div>}
</div>
);
}
}
export class Selection extends BaseSelection {}
export default themeable(
localeable(
uncontrollable(Selection, {
value: 'onChange'
})
)
);