Skip to content

Commit

Permalink
feat(Cascader): 添加搜索功能 (#715)
Browse files Browse the repository at this point in the history
  • Loading branch information
nullptr-z committed Mar 25, 2022
1 parent 1feba3a commit ce01687
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 29 deletions.
68 changes: 67 additions & 1 deletion packages/react-cascader/README.md
Expand Up @@ -57,7 +57,71 @@ const Demo = () => {

return (
<div style={{ width: 200 }}>
<Cascader allowClear={true} placeholder="请选择" value={[1, 4, 7]} option={options} onChange={(value, seleteds) => console.log(value, seleteds)} />
< Cascader
allowClear={true}
placeholder="请选择"
value={[1, 4, 7]}
option={options}
onChange={(value, seleteds) => console.log(value, seleteds)}
/>
</div>
)
};
ReactDOM.render(<Demo />, _mount_);
```

## 搜索选项

<!--rehype:bgWhite=true&codeSandbox=true&codePen=true-->
```jsx
import ReactDOM from 'react-dom';
import { Cascader } from 'uiw';

const Demo = () => {

const options = [
{
label: '上海市',
value: 1,
children: [
{
label: '徐汇区',
value: 4,
children: [
{ label: '半淞园路街道', value: 6 },
{ label: '南京东路街道', value: 7 },
{ label: '外滩街道', value: 8 },
]
},
]
},
{
label: '北京市',
value: 9,
children: [
{
label: '崇文区',
value: 12,
children: [
{ label: '东花市街道', value: 13 },
{ label: '体育馆路街道', value: 14 },
{ label: '前门街道', value: 15 },
]
},
]
},
];

return (
<div style={{ width: 200 }}>
<Cascader
allowClear={true}
placeholder="请选择"
value={[1, 4, 7]}
option={options}
onChange={(value, seleteds) => console.log(value, seleteds)}
onSearch={(text)=> console.log('text', text)}
/>
</div>
)
};
Expand Down Expand Up @@ -138,6 +202,7 @@ const options = [
children: (
<Cascader
allowClear={true}
onSearch={true}
placeholder="请选择"
option={options}
onChange={(value, seleteds) => console.log(value, seleteds)}
Expand Down Expand Up @@ -183,3 +248,4 @@ ReactDOM.render(<Demo />, _mount_);
| option | 选项菜单 | { value: String \| Number, label: React.ReactNode, children: Array<String \| Number>} | - | - |
| value | 指定当前选中的条目,多选时为一个数组 | String[] \| Number[] | - | - |
| onChange | 选中选项调用此函数 | function( isSeleted, value, selectedOptions) | - | - |
| onSearch | 开启搜索选项 | functionon(searchText) \| Boolean | - | v4.16.1 |
140 changes: 112 additions & 28 deletions packages/react-cascader/src/index.tsx
Expand Up @@ -8,20 +8,22 @@ import './style/index.less';

type ValueType = Array<string | number>;
type OptionType = { value: string | number; label: React.ReactNode; children?: Array<OptionType> };
type SearchOptionType = { label: string; options: Array<OptionType> };

export interface CascaderProps extends IProps, DropdownProps {
option?: Array<OptionType>;
value?: ValueType;
onChange?: (isSeleted: boolean, value: ValueType, selectedOptions: Array<OptionType>) => void;
onSearch?: boolean | ((searchText: string) => void);
allowClear?: boolean;
placeholder?: string;
isOpen?: boolean;
}

function Cascader(props: CascaderProps) {
const {
value,
onChange,
onSearch,

allowClear,
placeholder,
Expand All @@ -36,16 +38,48 @@ function Cascader(props: CascaderProps) {
const [innerIsOpen, setInnerIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<Array<OptionType>>([]);
const [selectIconType, setSelectIconType] = useState('');
const [searchText, setSearchText] = useState<string>('');
const [searchOn, setSearchOn] = useState(false);
const [inputValue, setInputValue] = useState('');
const [searchOption, setSearchOption] = useState<Array<SearchOptionType>>();

useEffect(() => {
const valueTemp: Array<OptionType> = [];
let optChildren = option;
value?.map((item) => {
const findOpt = optChildren.find((opt) => opt.value === item);
optChildren = findOpt?.children || [];
valueTemp.push({ label: item, value: item, ...findOpt });
if (onSearch) {
const tempOptions: Array<SearchOptionType> = [];
iteratorOption(option, (opt) => {
const label = opt.map((m) => m.label).join(' / ');
tempOptions.push({ label, options: opt });
});
setSearchOption(tempOptions);
}
}, [onSearch]);

const iteratorOption = (
options: Array<OptionType>,
cb: (opt: Array<OptionType>) => void,
opts: Array<OptionType> = [],
) => {
options.map((opt) => {
const optsTemp = [...opts, opt];
if (opt.children) {
iteratorOption(opt.children, cb, optsTemp);
} else {
cb?.(optsTemp);
}
});
setSelectedValue(valueTemp);
};

useEffect(() => {
if (value) {
const valueTemp: Array<OptionType> = [];
let optChildren = option;
value?.map((item) => {
const findOpt = optChildren.find((opt) => opt.value === item);
optChildren = findOpt?.children || [];
valueTemp.push({ label: item, value: item, ...findOpt });
});
setSelectedValue(valueTemp);
}
}, [value]);

function onVisibleChange(isOpen: boolean) {
Expand All @@ -62,6 +96,12 @@ function Cascader(props: CascaderProps) {
setSelectIconType(selectIconType);
}

const searchItemClick = (options: Array<OptionType>) => {
setSearchText('');
setInnerIsOpen(false);
handelChange(false, options);
};

const handleItemClick = (optionsItem: OptionType, level: number) => {
selectedValue.splice(level, selectedValue.length - level, optionsItem);

Expand All @@ -76,10 +116,21 @@ function Cascader(props: CascaderProps) {

const onClear = (e: React.MouseEvent<any, MouseEvent>) => {
e.stopPropagation();
console.log(123);
handelChange(false, []);
};

const handelSearch = (searchText: string) => {
setSearchText(searchText);
};

const inputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!innerIsOpen) {
setInnerIsOpen(!innerIsOpen);
}
const value = e.target.value;
onSearch && handelSearch(value);
};

const widths = (style?.width as number) * 0.5 || undefined;

const OptionIter = (option: Array<OptionType>, level: number = 0) => {
Expand Down Expand Up @@ -116,9 +167,10 @@ function Cascader(props: CascaderProps) {
) as JSX.Element;
};

const inputValue = useMemo(() => {
return selectedValue.map((opt) => opt.label).join(' / ');
}, [selectedValue.length]);
useEffect(() => {
const inputValue = selectedValue.map((opt) => opt.label).join(' / ');
setInputValue(inputValue);
}, [selectedValue]);

return (
<Dropdown
Expand All @@ -130,28 +182,60 @@ function Cascader(props: CascaderProps) {
onVisibleChange={onVisibleChange}
isOpen={innerIsOpen}
menu={
<div style={{ display: 'flex' }}>
{new Array(selectedValue.length + 1)
.fill(0)
.map((_, index) => {
const options = index ? selectedValue[index - 1]?.children! : option;
return OptionIter(options, index);
})
.filter((m) => !!m)}
</div>
!searchText ? (
<div style={{ display: 'flex' }}>
{new Array(selectedValue.length + 1)
.fill(0)
.map((_, index) => {
const options = index ? selectedValue[index - 1]?.children! : option;
return OptionIter(options, index);
})
.filter((m) => !!m)}
</div>
) : (
<Menu
bordered
style={{
minHeight: 25,
minWidth: style?.width,
overflowY: 'scroll',
width: style?.width,
}}
>
{!searchOption || searchOption.length === 0 ? (
<div style={{ color: '#c7c7c7', fontSize: 12 }}>{'没有数据'}</div>
) : (
searchOption
.filter((opt) => opt.label.includes(searchText.trim()))
.map((opt, index) => {
return (
<Menu.Item
key={index}
text={opt.label}
onClick={() => searchItemClick(opt.options)} //
/>
);
})
)}
</Menu>
)
}
>
<span onMouseLeave={() => renderSelectIcon('leave')} onMouseOver={() => renderSelectIcon('enter')}>
<Input
value={inputValue}
onChange={() => {}}
placeholder={placeholder}
value={searchOn ? searchText : inputValue}
onChange={inputChange}
placeholder={searchOn ? inputValue : placeholder}
style={{ width: style?.width }}
readOnly
onFocus={() => onSearch && setSearchOn(true)}
onBlur={() => onSearch && setSearchOn(false)}
readOnly={!onSearch}
addonAfter={
selectIconType === 'close' && (
<Icon type={`${selectIconType}`} onClick={onClear} className={`${prefixCls}-close`} />
)
<span style={{ width: 'auto' }}>
{selectIconType === 'close' && (
<Icon type={selectIconType} onClick={onClear} className={`${prefixCls}-close`} />
)}
</span>
}
/>
</span>
Expand Down

0 comments on commit ce01687

Please sign in to comment.