mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-30 02:58:05 +08:00
feat: cascader-select 组件升级 (#3450)
* feat: cascader-select 组件升级 * fix:无结果文案参数展示 * fix: review建议,优化节点选中的判断逻辑,移动端样式适配 * fix: 代码rebase问题,clear icon覆盖
This commit is contained in:
parent
ea50677a50
commit
ae905e12a4
@ -1179,6 +1179,8 @@
|
||||
--ResultBox-value--onHover-bg: rgba(0, 145, 255, 0.1);
|
||||
--ResultBox-value-bg: #f5f5f5;
|
||||
--ResultBox-value-color: #000;
|
||||
--ResultBox-value-clear-bg: #d4d6d9;
|
||||
--ResultBox-value-clear-hover-bg: #f5f5f5;
|
||||
|
||||
--Rating-inactive-color: #e6e6e8;
|
||||
--Rating-star-margin: #{px2rem(8px)};
|
||||
|
@ -2,9 +2,15 @@
|
||||
@include input-input();
|
||||
@include input-border();
|
||||
flex-wrap: wrap;
|
||||
padding: 0 px2rem(3px);
|
||||
padding: 0 px2rem(26px) 0 px2rem(3px);
|
||||
min-height: var(--Form-input-height);
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
|
||||
&.is-clearable {
|
||||
padding-right: px2rem(55px);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
border-color: var(--Form-input-onError-borderColor);
|
||||
@ -49,8 +55,46 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-pc-arrow {
|
||||
height: 100%;
|
||||
margin: auto 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform var(--animation-duration) ease;
|
||||
> svg {
|
||||
width: px2rem(10px);
|
||||
height: px2rem(10px);
|
||||
top: 0.125em;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-opened &-pc-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&-clear {
|
||||
@include input-clear();
|
||||
height: 100%;
|
||||
padding: px2rem(4px);
|
||||
margin: auto 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
background-color: unset;
|
||||
|
||||
&-wrap {
|
||||
width: px2rem(16px);
|
||||
height: px2rem(16px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-with-arrow {
|
||||
right: 33px;
|
||||
}
|
||||
}
|
||||
|
||||
> svg {
|
||||
@ -86,6 +130,9 @@
|
||||
> svg {
|
||||
width: px2rem(10px);
|
||||
height: px2rem(10px);
|
||||
&.icon {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,6 +181,20 @@
|
||||
border: none;
|
||||
justify-content: flex-end;
|
||||
|
||||
.#{$ns}ResultBox-clear {
|
||||
@include input-clear();
|
||||
width: px2rem(26px);
|
||||
height: px2rem(26px);
|
||||
margin: 0 px2rem(-2px);
|
||||
margin-left: auto;
|
||||
padding: px2rem(4px);
|
||||
position: unset;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.#{$ns}ResultBox-arrow {
|
||||
margin-right: var(--gap-xs);
|
||||
// margin-left: var(--gap-xs);
|
||||
|
@ -7,7 +7,7 @@
|
||||
@include input-border();
|
||||
&-optionArrowRight {
|
||||
display: inline-block;
|
||||
padding-right: px2rem(10px);
|
||||
padding-right: px2rem(40px);
|
||||
|
||||
svg {
|
||||
width: px2rem(12px);
|
||||
@ -61,12 +61,15 @@
|
||||
|
||||
> .#{$ns}NestedSelect-optionLabel {
|
||||
flex: 1;
|
||||
|
||||
height: px2rem(32px);
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--text--muted-color);
|
||||
}
|
||||
}
|
||||
.#{$ns}NestedSelect-optionLabel-highlight {
|
||||
color: var(--Form-select-menu-onActive-color);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--Form-select-menu-onActive-color);
|
||||
@ -82,8 +85,13 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.checkall {
|
||||
border-bottom: px2rem(1px) solid #eceff8;
|
||||
&.no-result {
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
color: unset;
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@
|
||||
padding: 0 var(--gap-xs);
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
text-overflow:ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
@ -136,6 +136,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
|
||||
--Form-input-onDisabled-bg: #{$G10};
|
||||
--Form-input-onDisabled-borderColor: #{$G8};
|
||||
--Form-input-onDisabled-color: #{$G5};
|
||||
--Form-input-iconColor: #{$G5};
|
||||
--Form-input-paddingX: #{px2rem(10px)};
|
||||
--Form-description-color: #999;
|
||||
--Form--horizontal-label-whiteSpace: normal;
|
||||
@ -622,4 +623,9 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
|
||||
|
||||
// Formula
|
||||
--InputFormula-code-bgColor: #{$G10};
|
||||
|
||||
// ResultBox
|
||||
--ResultBox-value-bg: #{$G10};
|
||||
--ResultBox-value-clear-bg: #{$G8};
|
||||
--ResultBox-value-clear-hover-bg: #{$G9};
|
||||
}
|
||||
|
@ -17,9 +17,11 @@ export interface ResultBoxProps
|
||||
result?: Array<any> | any;
|
||||
itemRender: (value: any) => JSX.Element | string;
|
||||
onResultChange?: (value: Array<any>) => void;
|
||||
onClear?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
allowInput?: boolean;
|
||||
inputPlaceholder: string;
|
||||
useMobileUI?: boolean;
|
||||
hasDropDownArrow?: boolean;
|
||||
}
|
||||
|
||||
export class ResultBox extends React.Component<ResultBoxProps> {
|
||||
@ -52,8 +54,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
|
||||
@autobind
|
||||
clearValue(e: React.MouseEvent<any>) {
|
||||
e.preventDefault();
|
||||
const onResultChange = this.props.onResultChange;
|
||||
onResultChange && onResultChange([]);
|
||||
this.props.onClear && this.props.onClear(e);
|
||||
this.props.onResultChange && this.props.onResultChange([]);
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -117,6 +119,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
|
||||
onBlur,
|
||||
borderMode,
|
||||
useMobileUI,
|
||||
hasDropDownArrow,
|
||||
onClear,
|
||||
...rest
|
||||
} = this.props;
|
||||
const isFocused = this.state.isFocused;
|
||||
@ -129,6 +133,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
|
||||
'is-disabled': disabled,
|
||||
'is-error': hasError,
|
||||
'is-clickable': onResultClick,
|
||||
'is-clearable': clearable,
|
||||
'is-mobile': mobileUI,
|
||||
[`ResultBox--border${ucFirst(borderMode)}`]: borderMode
|
||||
})}
|
||||
@ -183,10 +188,22 @@ export class ResultBox extends React.Component<ResultBoxProps> {
|
||||
{clearable &&
|
||||
!disabled &&
|
||||
(Array.isArray(result) ? result.length : result) ? (
|
||||
<a onClick={this.clearValue} className={cx('ResultBox-clear')}>
|
||||
<Icon icon="input-clear" className="icon" />
|
||||
<a
|
||||
onClick={this.clearValue}
|
||||
className={cx('ResultBox-clear', {
|
||||
'ResultBox-clear-with-arrow': hasDropDownArrow
|
||||
})}
|
||||
>
|
||||
<div className={cx('ResultBox-clear-wrap')}>
|
||||
<Icon icon="input-clear" className="icon" />
|
||||
</div>
|
||||
</a>
|
||||
) : null}
|
||||
{hasDropDownArrow && !mobileUI && (
|
||||
<span className={cx('ResultBox-pc-arrow')}>
|
||||
<Icon icon="caret" className="icon" />
|
||||
</span>
|
||||
)}
|
||||
{!allowInput && mobileUI ? (
|
||||
<span className={cx('ResultBox-arrow')}>
|
||||
<Icon icon="caret" className="icon" />
|
||||
|
@ -126,6 +126,13 @@ export default class NestedSelectControl extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleResultClear() {
|
||||
this.setState({
|
||||
inputValue: undefined
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
close() {
|
||||
this.setState({
|
||||
@ -162,23 +169,56 @@ export default class NestedSelectControl extends React.Component<
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderValue(item: Option, key?: any) {
|
||||
const {classnames: cx, labelField, options, hideNodePathLabel} = this.props;
|
||||
renderValue(option: Option, key?: any) {
|
||||
const {
|
||||
classnames: cx,
|
||||
labelField,
|
||||
valueField,
|
||||
options,
|
||||
hideNodePathLabel
|
||||
} = this.props;
|
||||
const inputValue = this.state.inputValue;
|
||||
const regexp = string2regExp(inputValue || '');
|
||||
|
||||
if (hideNodePathLabel) {
|
||||
return item[labelField || 'label'];
|
||||
return option[labelField || 'label'];
|
||||
}
|
||||
const ancestors = getTreeAncestors(options, item, true);
|
||||
const ancestors = getTreeAncestors(options, option, true);
|
||||
|
||||
return (
|
||||
<span className={cx('Select-valueLabel')} key={key}>
|
||||
{`${
|
||||
ancestors
|
||||
? ancestors
|
||||
.map(item => `${item[labelField || 'label']}`)
|
||||
.join(' / ')
|
||||
: item[labelField || 'label']
|
||||
}`}
|
||||
<span
|
||||
className={cx('Select-valueLabel')}
|
||||
key={key || option[valueField || 'value']}
|
||||
>
|
||||
{ancestors
|
||||
? ancestors.map((item, index) => {
|
||||
const label = item[labelField || 'label'];
|
||||
const isEnd = index === ancestors.length - 1;
|
||||
const unmatchText = label.split(regexp || '');
|
||||
let pointer = 0;
|
||||
return (
|
||||
<span key={index}>
|
||||
{regexp.test(label)
|
||||
? unmatchText.map((text: string, textIndex: number) => {
|
||||
const current = pointer;
|
||||
pointer += text.length || inputValue?.length || 0;
|
||||
return (
|
||||
<span
|
||||
key={textIndex}
|
||||
className={cx({
|
||||
'NestedSelect-optionLabel-highlight': !text
|
||||
})}
|
||||
>
|
||||
{text || label.slice(current, pointer)}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
: label}
|
||||
{!isEnd && '>'}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
: option[labelField || 'label']}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -553,6 +593,96 @@ export default class NestedSelectControl extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
renderSearchResult() {
|
||||
const {stack, inputValue} = this.state;
|
||||
const {
|
||||
classnames: cx,
|
||||
options: propOptions,
|
||||
labelField,
|
||||
cascade,
|
||||
selectedOptions,
|
||||
multiple,
|
||||
disabled,
|
||||
onlyChildren,
|
||||
noResultsText
|
||||
} = this.props;
|
||||
const regexp = string2regExp(inputValue || '');
|
||||
const flattenTreeWithNodes = flattenTree(stack[0]).filter(option => {
|
||||
return regexp.test(option[labelField || 'label']);
|
||||
});
|
||||
|
||||
// 一个stack一个menu
|
||||
const resultBody = (
|
||||
<div className={cx('NestedSelect-menu')}>
|
||||
{flattenTreeWithNodes.length ? (
|
||||
flattenTreeWithNodes.map((option, index) => {
|
||||
const ancestors = getTreeAncestors(propOptions, option as any);
|
||||
|
||||
const uncheckable = cascade
|
||||
? false
|
||||
: multiple &&
|
||||
ancestors?.some(item => !!~selectedOptions.indexOf(item));
|
||||
|
||||
let isNodeDisabled =
|
||||
uncheckable ||
|
||||
option.disabled ||
|
||||
!!disabled ||
|
||||
ancestors?.some(item => !!item.disabled);
|
||||
|
||||
let isChildrenChecked = !!(
|
||||
option.children && this.partialChecked(option.children)
|
||||
);
|
||||
|
||||
let isChecked = uncheckable || !!~selectedOptions.indexOf(option);
|
||||
|
||||
if (
|
||||
!isChecked &&
|
||||
onlyChildren &&
|
||||
option.children &&
|
||||
this.allChecked(option.children)
|
||||
) {
|
||||
isChecked = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('NestedSelect-option', {
|
||||
'is-active':
|
||||
!isNodeDisabled &&
|
||||
(isChecked || (!cascade && isChildrenChecked))
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
<div
|
||||
className={cx('NestedSelect-optionLabel', {
|
||||
'is-disabled': isNodeDisabled
|
||||
})}
|
||||
onClick={() => {
|
||||
!isNodeDisabled &&
|
||||
(multiple
|
||||
? this.handleCheck(option, option.value)
|
||||
: this.handleOptionClick(option));
|
||||
}}
|
||||
>
|
||||
{this.renderValue(option, option.value)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div
|
||||
className={cx('NestedSelect-option', {
|
||||
'no-result': true
|
||||
})}
|
||||
>
|
||||
{noResultsText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return resultBody;
|
||||
}
|
||||
|
||||
onMouseEnter(option: Option, index: number, e: MouseEvent) {
|
||||
let {stack} = this.state;
|
||||
index = index + 1;
|
||||
@ -579,6 +709,7 @@ export default class NestedSelectControl extends React.Component<
|
||||
options,
|
||||
render
|
||||
} = this.props;
|
||||
const isSearch = !!this.state.inputValue;
|
||||
let noResultsText: any = this.props.noResultsText;
|
||||
|
||||
if (noResultsText) {
|
||||
@ -590,7 +721,9 @@ export default class NestedSelectControl extends React.Component<
|
||||
{(ref: any) => {
|
||||
return (
|
||||
<div className={cx('NestedSelect-menuOuter')} ref={ref}>
|
||||
{options.length ? (
|
||||
{isSearch ? (
|
||||
this.renderSearchResult()
|
||||
) : options.length ? (
|
||||
this.renderOptions()
|
||||
) : (
|
||||
<div className={cx('NestedSelect-noResult')}>
|
||||
@ -662,12 +795,14 @@ export default class NestedSelectControl extends React.Component<
|
||||
value={this.state.inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
onResultChange={this.handleResultChange}
|
||||
onClear={this.handleResultClear}
|
||||
itemRender={this.renderValue}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.handleInputKeyDown}
|
||||
clearable={clearable}
|
||||
hasDropDownArrow={true}
|
||||
allowInput={searchable}
|
||||
inputPlaceholder={''}
|
||||
>
|
||||
@ -703,3 +838,7 @@ export default class NestedSelectControl extends React.Component<
|
||||
type: 'nested-select'
|
||||
})
|
||||
export class NestedSelectControlRenderer extends NestedSelectControl {}
|
||||
@OptionsControl({
|
||||
type: 'cascader-select'
|
||||
})
|
||||
export class CascaderSelectControlRenderer extends NestedSelectControl {}
|
||||
|
Loading…
Reference in New Issue
Block a user