288 lines
6.9 KiB
TypeScript
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'
|
|
})
|
|
)
|
|
);
|