Merge pull request #339 from 2betop/master

优化 select & Tree
This commit is contained in:
liaoxuezhi 2019-11-10 18:13:07 +08:00 committed by GitHub
commit 2529cfe18d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1655 additions and 1114 deletions

View File

@ -146,6 +146,9 @@ $gap-base: px2rem(15px) !default;
$gap-md: px2rem(20px) !default; $gap-md: px2rem(20px) !default;
$gap-lg: px2rem(30px) !default; $gap-lg: px2rem(30px) !default;
$icon-color: $gray600 !default;
$icon-onHover-color: $gray900 !default;
// Components // Components
$scrollbar-width: px2rem(17px) !default; $scrollbar-width: px2rem(17px) !default;
@ -446,7 +449,7 @@ $Table-thead-fontSize: $fontSizeBase !default;
$Table-thead-color: $text--loud-color !default; $Table-thead-color: $text--loud-color !default;
$Table-thead-borderColor: $Table-borderColor !default; $Table-thead-borderColor: $Table-borderColor !default;
$Table-thead-borderWidth: $Table-borderWidth !default; $Table-thead-borderWidth: $Table-borderWidth !default;
$Table-thead-iconColor: $text--muted-color !default; $Table-thead-iconColor: $icon-color !default;
$TableCell-height: px2rem(40px) !default; $TableCell-height: px2rem(40px) !default;
$TableCell-paddingX: $gap-sm !default; $TableCell-paddingX: $gap-sm !default;
$TableCell--edge-paddingX: $gap-md !default; $TableCell--edge-paddingX: $gap-md !default;
@ -554,19 +557,19 @@ $ListItem-onModified-borderColor: darken($ListItem-onModified-bg, 10%) !default;
$ListItem-onDragging-opacity: 0.1 !default; $ListItem-onDragging-opacity: 0.1 !default;
// QuickEdit // QuickEdit
$QuickEdit-iconColor: $text--muted-color !default; $QuickEdit-iconColor: $icon-color !default;
$QuickEdit-onHover-iconColor: $text-color !default; $QuickEdit-onHover-iconColor: $icon-onHover-color !default;
$QuickEdit-onFocus-borderColor: $info !default; $QuickEdit-onFocus-borderColor: $info !default;
$QuickEdit-onFocus-borderWidth: $borderWidth !default; $QuickEdit-onFocus-borderWidth: $borderWidth !default;
// Copyable // Copyable
$Copyable-iconColor: $text--muted-color !default; $Copyable-iconColor: $icon-color !default;
$Copyable-onHover-iconColor: $text-color !default; $Copyable-onHover-iconColor: $icon-onHover-color !default;
// PopOverAble // PopOverAble
$PopOverAble-iconColor: $text--muted-color !default; $PopOverAble-iconColor: $icon-color !default;
$PopOverAble-onHover-iconColor: $text-color !default; $PopOverAble-onHover-iconColor: $icon-onHover-color !default;
// PopOver // PopOver
$PopOver-bg: white !default; $PopOver-bg: white !default;
@ -574,8 +577,8 @@ $PopOver-bg: white !default;
// Remark // Remark
$Remark-width: 1rem !default; $Remark-width: 1rem !default;
$Remark-icon-fontSize: $fontSizeBase !default; $Remark-icon-fontSize: $fontSizeBase !default;
$Remark-iconColor: $text--muted-color !default; $Remark-iconColor: $icon-color !default;
$Remark-onHover-iconColor: $text-color !default; $Remark-onHover-iconColor: $icon-onHover-color !default;
$Remark-bg: transparent !default; $Remark-bg: transparent !default;
$Remark-onHover-bg: transparent !default; $Remark-onHover-bg: transparent !default;
$Remark-borderWidth: 0 !default; $Remark-borderWidth: 0 !default;
@ -618,7 +621,7 @@ $Form-input-onActive-color: $info !default;
$Form-input-borderRadius: $borderRadius !default; $Form-input-borderRadius: $borderRadius !default;
$Form-input-borderColor: $borderColor !default; $Form-input-borderColor: $borderColor !default;
$Form-input-borderWidth: px2rem(1px) !default; $Form-input-borderWidth: px2rem(1px) !default;
$Form-input-iconColor: #999 !default; $Form-input-iconColor: $icon-color !default;
$Form-input-onFocused-borderColor: $info !default; $Form-input-onFocused-borderColor: $info !default;
$Form-input-onFocused-bg: $Form-input-bg !default; $Form-input-onFocused-bg: $Form-input-bg !default;
$Form-input-onError-borderColor: $danger !default; $Form-input-onError-borderColor: $danger !default;
@ -674,18 +677,18 @@ $Form-select-onFocused-color: $Form-select-color !default;
$Form-select-onFocused-borderColor: $Form-input-onFocused-borderColor !default; $Form-select-onFocused-borderColor: $Form-input-onFocused-borderColor !default;
$Form-select-onError-borderColor: $Form-input-onError-borderColor !default; $Form-select-onError-borderColor: $Form-input-onError-borderColor !default;
$Form-selectOption-height: $Form-input-height !default; $Form-selectOption-height: $Form-input-height !default;
$Form-selectValue-color: #007eff !default; $Form-selectValue-color: $info !default;
$Form-selectValue-bg: saturate(lighten($Form-selectValue-color, 40%), 2.5%) !default; $Form-selectValue-bg: saturate(lighten($Form-selectValue-color, 40%), 2.5%) !default;
$Form-selectValue-borderColor: saturate(lighten($Form-selectValue-color, 30%), 2.5%) !default; $Form-selectValue-borderColor: saturate(lighten($Form-selectValue-color, 30%), 2.5%) !default;
$Form-selectValue-fontSize: $fontSizeSm !default; $Form-selectValue-fontSize: $fontSizeSm !default;
$Form-select-caret-vender: 'FontAwesome' !default; $Form-select-caret-vender: 'FontAwesome' !default;
$Form-select-caret-icon: '\f0d7' !default; $Form-select-caret-icon: '\f0d7' !default;
$Form-select-caret-fontSize: $fontSizeBase !default; $Form-select-caret-fontSize: $fontSizeBase !default;
$Form-select-caret-iconColor: $Form-input-iconColor !default; $Form-select-caret-iconColor: $icon-color !default;
$Form-select-caret-onHover-iconColor: $Form-input-iconColor !default; $Form-select-caret-onHover-iconColor: $icon-onHover-color !default;
$Form-select-outer-borderWidth: px2rem(1px) !default; $Form-select-outer-borderWidth: px2rem(1px) !default;
$Form-select-outer-boxShadow: none !default; $Form-select-outer-boxShadow: none !default;
$Form-select-outer-top: 100% !default; $Form-select-input-fontSize: $fontSizeSm !default;
$Form-select-menu-height: $Form-input-height !default; $Form-select-menu-height: $Form-input-height !default;
$Form-select-menu-bg: $white !default; $Form-select-menu-bg: $white !default;
$Form-select-menu-color: $Form-select-color !default; $Form-select-menu-color: $Form-select-color !default;
@ -696,6 +699,8 @@ $Form-select-menu-onActive-bg: transparent !default;
$Form-select-menu-onDisabled-color: $text--muted-color !default; $Form-select-menu-onDisabled-color: $text--muted-color !default;
$Form-select-menu-onDisabled-bg: transparent !default; $Form-select-menu-onDisabled-bg: transparent !default;
$Form-select-checkall-bottomBorder: #eceff8 !default; $Form-select-checkall-bottomBorder: #eceff8 !default;
$Form-select-popoverGap: 0 !default;
$Form-select-search-height: $Form-select-menu-height !default;
// InputGroup // InputGroup
$InputGroup-height: $Form-input-height !default; $InputGroup-height: $Form-input-height !default;
@ -713,8 +718,8 @@ $InputGroup-select-bg: $white !default;
$InputGroup-select-onFocused-bg: $white !default; $InputGroup-select-onFocused-bg: $white !default;
$InputGroup-select-color: $Form-select-color !default; $InputGroup-select-color: $Form-select-color !default;
$InputGroup-select-onFocused-color: $Form-select-onFocused-color !default; $InputGroup-select-onFocused-color: $Form-select-onFocused-color !default;
$InputGroup-select-arrowColor: $Form-select-caret-iconColor !default; $InputGroup-select-arrowColor: $icon-color !default;
$InputGroup-select-onFocused-arrowColor: $Form-select-caret-iconColor !default; $InputGroup-select-onFocused-arrowColor: $icon-onHover-color !default;
$InputGroup-button-borderWidth: $borderWidth !default; $InputGroup-button-borderWidth: $borderWidth !default;
$InputGroup-button-borderColor: $Form-input-borderColor !default; $InputGroup-button-borderColor: $Form-input-borderColor !default;
$InputGroup-button-borderRadius: $borderRadius !default; $InputGroup-button-borderRadius: $borderRadius !default;
@ -892,6 +897,8 @@ $Checkbox-color: $Form-input-borderColor !default;
$Checkbox-onHover-color: $info !default; $Checkbox-onHover-color: $info !default;
$Checkbox--sm-size: px2rem(16px) !default; $Checkbox--sm-size: px2rem(16px) !default;
$Checkbox--sm-inner-size: $Checkbox--sm-size/2 !default;
$Checkbox--sm--full-inner-size: px2rem(10px) !default;
$Checkbox-gb: #fff !default; $Checkbox-gb: #fff !default;
$Checkbox-borderRadius: $borderRadius !default; $Checkbox-borderRadius: $borderRadius !default;
@ -949,8 +956,8 @@ $DatePicker-fontSize: $Form-input-fontSize !default;
$DatePicker-paddingX: px2rem(12px) !default; $DatePicker-paddingX: px2rem(12px) !default;
$DatePicker-paddingY: ($DatePicker-height - $DatePicker-lineHeight * $DatePicker-fontSize)/2 - $DatePicker-borderWidth !default; $DatePicker-paddingY: ($DatePicker-height - $DatePicker-lineHeight * $DatePicker-fontSize)/2 - $DatePicker-borderWidth !default;
$DatePicker-placeholderColor: $Form-input-placeholderColor !default; $DatePicker-placeholderColor: $Form-input-placeholderColor !default;
$DatePicker-iconColor: $gray600 !default; $DatePicker-iconColor: $icon-color !default;
$DatePicker-onHover-iconColor: darken($DatePicker-iconColor, 10%) !default; $DatePicker-onHover-iconColor: $icon-onHover-color !default;
$DatePicker-onFocused-borderColor: $Form-input-onFocused-borderColor !default; $DatePicker-onFocused-borderColor: $Form-input-onFocused-borderColor !default;
$DatePicker-toggler-vendor: 'FontAwesome' !default; $DatePicker-toggler-vendor: 'FontAwesome' !default;
$DatePicker-toggler-fontSize: $Form-fontSize !default; $DatePicker-toggler-fontSize: $Form-fontSize !default;
@ -1041,7 +1048,7 @@ $ListControl-item-onDisabled-bg: $ListControl-item-bg !default;
// Combo // Combo
$Combo-toolbarBtn-lineHeight: 1 !default; $Combo-toolbarBtn-lineHeight: 1 !default;
$Combo-toolbarBtn-height: px2rem(20px) !default; $Combo-toolbarBtn-height: px2rem(20px) !default;
$Combo-toolbarBtn-color: $Form-input-iconColor !default; $Combo-toolbarBtn-color: $icon-color !default;
$Combo-toolbarBtn-paddingX: px2rem(5px) !default; $Combo-toolbarBtn-paddingX: px2rem(5px) !default;
$Combo-toolbarBtn-paddingY: px2rem(2px) !default; $Combo-toolbarBtn-paddingY: px2rem(2px) !default;
@ -1261,7 +1268,7 @@ $TransferSelect-heading-borderBottom: $borderWidth solid $borderColor !default;
// Tree // Tree
$TreeSelect-popover-bg: #fff !default; $TreeSelect-popover-bg: #fff !default;
$Tree-indent: px2rem(20px) !default; $Tree-indent: px2rem(20px) !default;
$Tree-itemArrowWidth: px2rem(10px) !default; $Tree-itemArrowWidth: px2rem(16px) !default;
$Tree-arrowVendor: 'FontAwesome' !default; $Tree-arrowVendor: 'FontAwesome' !default;
$Tree-unfoldedArrowContent: '\f107' !default; $Tree-unfoldedArrowContent: '\f107' !default;
$Tree-foldedArrowContent: '\f105' !default; $Tree-foldedArrowContent: '\f105' !default;
@ -1271,7 +1278,10 @@ $Tree-leafIconVendor: 'FontAwesome' !default;
$Tree-leafIconContent: '\f15b' !default; $Tree-leafIconContent: '\f15b' !default;
$Tree-folderIconVendor: 'FontAwesome' !default; $Tree-folderIconVendor: 'FontAwesome' !default;
$Tree-folderIconContent: '\f07b' !default; $Tree-folderIconContent: '\f07b' !default;
$Tree-itemText--onChecked-color: $Form-selectValue-color !default; $Tree-itemLabel--onChecked-color: $Form-selectValue-color !default;
$Tree-itemHeight: px2rem(30px) !default;
$Tree-item-onHover-bg: rgba(0, 126, 255, 0.08) !default;
$Tree-inputHeight: $Form-input-height * 0.85 !default;
// IconPicker // IconPicker
$IconPicker-tabs-bg: #f0f3f4 !default; $IconPicker-tabs-bg: #f0f3f4 !default;
@ -1339,8 +1349,8 @@ $Carousel-imageTitle-bottom: px2rem(45px) !default;
$Carousel-imageDescription-bottom: px2rem(25px) !default; $Carousel-imageDescription-bottom: px2rem(25px) !default;
// Picker // Picker
$Picker-iconColor: $gray600 !default; $Picker-iconColor: $icon-color !default;
$Picker-onHover-iconColor: darken($Picker-iconColor, 10%) !default; $Picker-onHover-iconColor: $icon-onHover-color !default;
$Picker-btn-vendor: 'FontAwesome' !default; $Picker-btn-vendor: 'FontAwesome' !default;
$Picker-btn-fontSize: $Form-fontSize !default; $Picker-btn-fontSize: $Form-fontSize !default;
$Picker-btn-icon: '\f2d2' !default; $Picker-btn-icon: '\f2d2' !default;

View File

@ -8,12 +8,12 @@
position: absolute; position: absolute;
} }
&:hover input:not(:disabled) + i { &:hover input:not(:disabled)+i {
border-color: $Checkbox-onHover-color; border-color: $Checkbox-onHover-color;
// box-shadow: 0 0 px2rem(1px) $Checkbox-onHover-color inset; // box-shadow: 0 0 px2rem(1px) $Checkbox-onHover-color inset;
} }
> i { >i {
cursor: pointer; cursor: pointer;
line-height: 1; line-height: 1;
background: $Checkbox-gb; background: $Checkbox-gb;
@ -21,7 +21,7 @@
vertical-align: middle; vertical-align: middle;
position: relative; position: relative;
+ span { +span {
margin-left: $Checkbox-gap; margin-left: $Checkbox-gap;
cursor: pointer; cursor: pointer;
@ -39,6 +39,8 @@
height: 0px; height: 0px;
background-color: transparent; background-color: transparent;
transition: all 0.2s; transition: all 0.2s;
transform-origin: 50% 50%;
transform: translate(-50%, -50%);
} }
} }
@ -48,19 +50,17 @@
input { input {
margin-left: -$Checkbox-size; margin-left: -$Checkbox-size;
&:checked + i { &:checked+i {
border-color: $Checkbox-onHover-color; border-color: $Checkbox-onHover-color;
&:before { &:before {
left: ($Checkbox-size - px2rem(2px) - $Checkbox-inner-size) / 2;
top: ($Checkbox-size - px2rem(2px) - $Checkbox-inner-size) / 2;
width: $Checkbox-inner-size; width: $Checkbox-inner-size;
height: $Checkbox-inner-size; height: $Checkbox-inner-size;
background: $Checkbox-onHover-color; background: $Checkbox-onHover-color;
} }
} }
&[disabled] + i { &[disabled]+i {
border-color: lighten($Checkbox-color, 5%); border-color: lighten($Checkbox-color, 5%);
cursor: not-allowed; cursor: not-allowed;
@ -70,13 +70,13 @@
} }
} }
&[disabled] + i + span { &[disabled]+i+span {
cursor: not-allowed; cursor: not-allowed;
color: $text--muted-color; color: $text--muted-color;
} }
} }
> i { >i {
width: $Checkbox-size; width: $Checkbox-size;
height: $Checkbox-size; height: $Checkbox-size;
border: px2rem(1px) solid $Checkbox-color; border: px2rem(1px) solid $Checkbox-color;
@ -87,25 +87,16 @@
} }
&--full.#{$ns}Checkbox--checkbox { &--full.#{$ns}Checkbox--checkbox {
&:not(:disabled) + i:hover { &:not(:disabled)+i:hover {
border-color: $Checkbox-color; border-color: $Checkbox-color;
} }
input { input {
&:checked + i { &:checked+i {
border-color: $Checkbox-onHover-color; border-color: $Checkbox-onHover-color;
background: $Checkbox-onHover-color; background: $Checkbox-onHover-color;
&:before { &:before {
// todo 后面自动计算
@if $ns== 'cxd-' {
top: px2rem(2px);
left: px2rem(1px);
} @else {
top: px2rem(5px);
left: ($Checkbox-size - $Checkbox--full-inner-size) /2;
}
width: $Checkbox--full-inner-size; width: $Checkbox--full-inner-size;
height: $Checkbox--full-inner-size / 2; height: $Checkbox--full-inner-size / 2;
border-color: $white; border-color: $white;
@ -120,13 +111,13 @@
// } // }
// } // }
&:checked[disabled] + i { &:checked[disabled]+i {
border-color: lighten($Checkbox-color, 5%); border-color: lighten($Checkbox-color, 5%);
background-color: lighten($Checkbox-color, 5%); background-color: lighten($Checkbox-color, 5%);
} }
} }
> i { >i {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -138,9 +129,9 @@
width: 0; width: 0;
height: 0; height: 0;
border-color: transparent; border-color: transparent;
transition: all 0.2s; transition: width 0.2s, height 0.2s, transform 0.2s;
border-width: 0 0 px2rem(2px) px2rem(2px); border-width: 0 0 px2rem(2px) px2rem(2px);
transform: rotate(-40deg); transform: translate(-50%, -60%) rotate(-40deg);
border-style: solid; border-style: solid;
} }
} }
@ -152,12 +143,10 @@
input { input {
margin-left: -$Radio-size; margin-left: -$Radio-size;
&:checked + i { &:checked+i {
border-color: $Radio-onHover-color; border-color: $Radio-onHover-color;
&:before { &:before {
left: ($Radio-size - px2rem(2px) - $Radio-inner-size) / 2;
top: ($Radio-size - px2rem(2px) - $Radio-inner-size) / 2;
width: $Radio-inner-size; width: $Radio-inner-size;
height: $Radio-inner-size; height: $Radio-inner-size;
background-color: $Radio-onHover-color; background-color: $Radio-onHover-color;
@ -165,7 +154,7 @@
} }
} }
&[disabled] + i { &[disabled]+i {
border-color: lighten($Radio-color, 5%); border-color: lighten($Radio-color, 5%);
cursor: not-allowed; cursor: not-allowed;
@ -174,13 +163,13 @@
} }
} }
&[disabled] + i + span { &[disabled]+i+span {
cursor: not-allowed; cursor: not-allowed;
color: $text--muted-color; color: $text--muted-color;
} }
} }
> i { >i {
cursor: pointer; cursor: pointer;
width: $Radio-size; width: $Radio-size;
height: $Radio-size; height: $Radio-size;
@ -192,30 +181,41 @@
} }
&--sm { &--sm {
margin-right: px2rem(5px); padding-left: $Checkbox--sm-size;
input { input {
&:checked + i { margin-left: -$Checkbox--sm-size;
&:checked+i {
&:before { &:before {
left: $Checkbox--sm-size / 4 - px2rem(1px); width: $Checkbox--sm-inner-size;
top: $Checkbox--sm-size / 4 - px2rem(1px); height: $Checkbox--sm-inner-size;
width: $Checkbox--sm-size / 2;
height: $Checkbox--sm-size / 2;
} }
} }
} }
> i { >i {
width: $Checkbox--sm-size; width: $Checkbox--sm-size;
height: $Checkbox--sm-size; height: $Checkbox--sm-size;
margin-left: -$Checkbox--sm-size; margin-left: -$Checkbox--sm-size;
+ span { +span {
margin-left: $gap-xs; margin-left: $gap-xs;
} }
} }
} }
&--sm.#{$ns}Checkbox--full {
input {
&:checked+i {
&:before {
width: $Checkbox--sm--full-inner-size;
height: $Checkbox--sm--full-inner-size / 2;
}
}
}
}
&-desc { &-desc {
color: $text--muted-color; color: $text--muted-color;
margin-left: $Checkbox-gap; margin-left: $Checkbox-gap;
@ -244,13 +244,14 @@
.#{$ns}RadiosControl-group, .#{$ns}RadiosControl-group,
.#{$ns}CheckboxesControl-group { .#{$ns}CheckboxesControl-group {
.#{$ns}RadiosControl-group, .#{$ns}RadiosControl-group,
.#{$ns}CheckboxesControl-group { .#{$ns}CheckboxesControl-group {
padding-left: px2rem(80px); padding-left: px2rem(80px);
@include clearfix(); @include clearfix();
> .#{$ns}RadiosControl-groupLabel, >.#{$ns}RadiosControl-groupLabel,
> .#{$ns}CheckboxesControl-groupLabel { >.#{$ns}CheckboxesControl-groupLabel {
float: left; float: left;
width: px2rem(80px); width: px2rem(80px);
margin-left: px2rem(-80px); margin-left: px2rem(-80px);
@ -261,4 +262,4 @@
.#{$ns}RadiosControl-groupLabel, .#{$ns}RadiosControl-groupLabel,
.#{$ns}CheckboxesControl-groupLabel { .#{$ns}CheckboxesControl-groupLabel {
display: block; display: block;
} }

View File

@ -8,10 +8,7 @@
background: $Form-select-bg; background: $Form-select-bg;
border-radius: $Form-select-borderRadius; border-radius: $Form-select-borderRadius;
height: $Form-selectOption-height; height: $Form-selectOption-height;
$paddingY: ( $paddingY: ($Form-selectOption-height - $Form-input-lineHeight * $Form-input-fontSize - $Form-select-borderWidth * 2)/2;
$Form-selectOption-height - $Form-input-lineHeight *
$Form-input-fontSize - $Form-select-borderWidth * 2
)/2;
padding: $paddingY 0 $paddingY $Form-select-paddingX; padding: $paddingY 0 $paddingY $Form-select-paddingX;
cursor: pointer; cursor: pointer;
color: $Form-select-color; color: $Form-select-color;
@ -46,17 +43,7 @@
user-select: none; user-select: none;
} }
&-input {
cursor: pointer;
display: inline-block;
position: relative;
z-index: 2;
outline: none;
border: none;
background: transparent;
line-height: $Form-input-lineHeight;
height: $Form-input-lineHeight * $Form-input-fontSize;
}
&-value { &-value {
line-height: $Form-input-lineHeight * $Form-input-fontSize; line-height: $Form-input-lineHeight * $Form-input-fontSize;
@ -64,6 +51,7 @@
} }
&--searchable { &--searchable {
.#{$ns}Select-placeholder, .#{$ns}Select-placeholder,
.#{$ns}Select-value { .#{$ns}Select-value {
position: absolute; position: absolute;
@ -79,22 +67,21 @@
.#{$ns}Select-valueWrap { .#{$ns}Select-valueWrap {
margin-bottom: -$gap-xs; margin-bottom: -$gap-xs;
> input { >input {
display: inline-block; display: inline-block;
width: px2rem(100px); width: px2rem(100px);
margin-bottom: $gap-xs; margin-bottom: $gap-xs;
} }
} }
.#{$ns}Select-values + .#{$ns}Select-input { .#{$ns}Select-values+.#{$ns}Select-input {
transform: translateY(0); transform: translateY(0);
} }
.#{$ns}Select-value { .#{$ns}Select-value {
position: static; position: static;
user-select: none; user-select: none;
line-height: $Form-input-lineHeight * $Form-input-fontSize - line-height: $Form-input-lineHeight * $Form-input-fontSize - px2rem(2px);
px2rem(2px);
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
font-size: $Form-selectValue-fontSize; font-size: $Form-selectValue-fontSize;
@ -145,44 +132,50 @@
transform: rotate(180deg); transform: rotate(180deg);
} }
&-menuOuter {
position: absolute;
background: $Form-select-menu-bg;
color: $Form-select-menu-color;
border: $Form-select-outer-borderWidth solid
$Form-input-onFocused-borderColor;
left: px2rem(-1px);
right: px2rem(-1px);
min-width: 100%;
top: $Form-select-outer-top;
z-index: 10;
box-shadow: $Form-select-outer-boxShadow;
}
&-menu { &-menu {
max-height: px2rem(300px); max-height: px2rem(300px);
overflow: auto; overflow-y: auto;
overflow-x: hidden;
user-select: none; user-select: none;
} }
&-checkAll { &-input {
padding: ( cursor: pointer;
$Form-select-menu-height - $Form-input-lineHeight * outline: none;
$Form-input-fontSize - px2rem(2px) border: none;
)/2 $Form-select-paddingX; margin: 0 $Form-select-paddingX;
border-bottom: px2rem(1px) solid $Form-select-checkall-bottomBorder; height: $Form-select-search-height;
min-width: px2rem(100px); font-size: $Form-select-input-fontSize;
border-bottom: 1px solid $borderColor;
display: flex;
align-items: center;
label { // &.is-focused {
display: block; // border-color: $Form-input-onFocused-borderColor;
// }
>svg {
fill: #999;
width: px2rem(14px);
min-width: px2rem(14px);
height: px2rem(14px);
margin-right: px2rem(5px);
}
>input {
outline: none;
border: none;
flex-grow: 1;
background: transparent;
position: relative;
top: 0.125em;
} }
} }
&-option { &-option {
padding: ( cursor: pointer;
$Form-select-menu-height - $Form-input-lineHeight * min-width: px2rem(150px);
$Form-input-fontSize - px2rem(2px) padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
)/2 $Form-select-paddingX;
&.is-active { &.is-active {
color: $Form-select-menu-onActive-color; color: $Form-select-menu-onActive-color;
@ -202,12 +195,50 @@
&--placeholder { &--placeholder {
color: $Form-input-placeholderColor; color: $Form-input-placeholderColor;
} }
>label {
display: block;
}
>a {
float: right;
margin-left: px2rem(5px);
display: none;
}
&.is-highlight>a {
display: block;
}
}
&-noResult {
color: $Form-select-placeholderColor;
line-height: $Form-input-lineHeight;
font-size: $Form-select-input-fontSize;
user-select: none;
padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
} }
&-option-hl { &-option-hl {
color: $red; color: $red;
} }
&-addBtn {
display: block;
cursor: pointer;
padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
&:hover {
text-decoration: none;
}
>svg {
width: px2rem(14px);
height: px2rem(14px);
margin-right: $Checkbox-gap;
}
}
&.is-focused, &.is-focused,
&.is-opened { &.is-opened {
border-color: $Form-input-onFocused-borderColor; border-color: $Form-input-onFocused-borderColor;
@ -243,28 +274,34 @@
} }
.#{$ns}Select-popover { .#{$ns}Select-popover {
margin-top: -$Form-select-borderWidth; margin-top: $Form-select-popoverGap - $Form-select-outer-borderWidth;
background: $Form-select-menu-bg; background: $Form-select-menu-bg;
color: $Form-select-menu-color; color: $Form-select-menu-color;
border: $Form-select-outer-borderWidth solid border: $Form-select-outer-borderWidth solid $Form-input-onFocused-borderColor;
$Form-input-onFocused-borderColor;
box-shadow: $Form-select-outer-boxShadow; box-shadow: $Form-select-outer-boxShadow;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
min-width: px2rem(100px); min-width: px2rem(100px);
z-index: 2;
&.#{$ns}PopOver--leftTopLeftBottom {
margin-top: -($Form-select-popoverGap - $Form-select-outer-borderWidth);
}
} }
.#{$ns}SelectControl:not(.is-inline) > .#{$ns}Select { .#{$ns}SelectControl:not(.is-inline)>.#{$ns}Select {
display: flex; display: flex;
} }
// 需要能撑开 // 需要能撑开
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.#{$ns}Form-control--sizeXs > .#{$ns}Select,
.#{$ns}Form-control--sizeSm > .#{$ns}Select, .#{$ns}Form-control--sizeXs>.#{$ns}Select,
.#{$ns}Form-control--sizeMd > .#{$ns}Select, .#{$ns}Form-control--sizeSm>.#{$ns}Select,
.#{$ns}Form-control--sizeLg > .#{$ns}Select { .#{$ns}Form-control--sizeMd>.#{$ns}Select,
.#{$ns}Form-control--sizeLg>.#{$ns}Select {
min-width: 100%; min-width: 100%;
display: inline-flex !important; display: inline-flex !important;
} }
} }

View File

@ -1,34 +1,3 @@
@mixin tree-input {
> svg {
display: inline-block;
cursor: pointer;
position: relative;
top: 2px;
width: px2rem(16px);
height: px2rem(16px);
margin-left: px2rem(5px);
}
> input {
margin-left: px2rem(15px);
padding: px2rem(5px);
width: px2rem(150px);
height: px2rem(25px);
color: $Form-input-color;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
&:focus {
outline: none;
border: $borderWidth solid $info;
}
}
}
// todo
.#{$ns}TreeControl { .#{$ns}TreeControl {
border: 1px solid $Form-input-borderColor; border: 1px solid $Form-input-borderColor;
padding: 6px 12px; padding: 6px 12px;
@ -46,6 +15,7 @@
} }
.#{$ns}Tree { .#{$ns}Tree {
&-list, &-list,
&-sublist { &-sublist {
list-style: none; list-style: none;
@ -53,39 +23,24 @@
margin: 0; margin: 0;
} }
&-sublist { &-sublist.is-folded {
padding-left: $Tree-indent; display: none;
&.is-folded {
display: none;
}
> li {
@include tree-input;
}
} }
&-item { &-item {
line-height: px2rem(30px); line-height: $Tree-itemHeight;
position: relative; position: relative;
.#{$ns}Tree-item-icons { >div {
visibility: hidden;
transition: visibility .1s ease;
}
> a {
color: inherit;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
> span.#{$ns}Tree-item-icons { >.#{$ns}Tree-item-icons {
visibility: visible; visibility: visible;
} }
} }
> span > svg { >span>svg {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
@ -95,49 +50,114 @@
margin-left: px2rem(5px); margin-left: px2rem(5px);
} }
} }
&--isLeaf > a {
padding-left: $Tree-itemArrowWidth + $gap-xs;
}
&--isEdit {
display: inline-block;
@include tree-input;
}
} }
&-rootItem { &-rootItem {
> a > i { line-height: $Tree-itemHeight;
margin-left: 0 !important; }
&-item>div:hover>.#{$ns}Tree-item-icons,
&-rootItem>div:hover>.#{$ns}Tree-item-icons {
visibility: visible;
}
&-itemLabel {
&:hover {
background: $Tree-item-onHover-bg;
}
}
&-item-icons {
visibility: hidden;
transition: visibility .1s ease;
display: inline-block;
vertical-align: top;
height: $Tree-itemHeight;
line-height: $Tree-itemHeight;
>a {
display: inline-block;
vertical-align: middle;
margin-left: $gap-xs;
cursor: pointer;
>svg {
$svgSize: px2rem(16px);
width: $svgSize;
height: $svgSize;
top: 0.125 * $svgSize;
}
}
}
&-itemInput {
padding-left: $Tree-itemArrowWidth;
>a {
display: inline-block;
cursor: pointer;
margin-left: $gap-sm;
color: $icon-color;
&:hover {
color: $icon-onHover-color;
text-decoration: none;
}
} }
.#{$ns}Tree-addTop { >input {
height: px2rem(25px); outline: none;
line-height: px2rem(25px); background-color: $Form-input-bg;
cursor: pointer; border: $Form-input-borderWidth solid $Form-input-borderColor;
padding-left: $Tree-indent; border-radius: $Form-input-borderRadius;
> p { line-height: $Form-input-lineHeight;
> svg { padding: ($Tree-inputHeight - $Form-input-lineHeight * $Form-input-fontSize - px2rem(2px))/2 $Form-input-paddingX;
position: relative; font-size: $Form-input-fontSize;
top: 2px;
width: px2rem(16px); &::placeholder {
height: px2rem(16px); color: $Form-input-placeholderColor;
} user-select: none;
> span {
padding-left: px2rem(5px);
}
} }
&-input { &:focus {
@include tree-input border-color: $Form-input-onFocused-borderColor;
box-shadow: $Form-input-boxShadow;
@if $Form-input-onFocused-bg !=$Form-input-bg {
background-color: $Form-input-onFocused-bg;
}
} }
} }
} }
&-addTopBtn {
cursor: pointer;
height: $Tree-itemHeight;
line-height: $Tree-itemHeight;
display: block;
&:hover {
text-decoration: none;
}
&.is-disabled {
pointer-events: none;
color: $text--muted-color;
}
>svg {
$svgSize: px2rem(14px);
width: $svgSize;
height: $svgSize;
top: $svgSize * 0.125;
margin-right: $Tree-itemArrowWidth - px2rem(14px); // icon 的宽度是14px
}
}
&-itemArrow { &-itemArrow {
cursor: pointer; cursor: pointer;
width: $Tree-itemArrowWidth; width: $Tree-itemArrowWidth;
margin-right: $gap-xs; text-align: center;
display: inline-block; display: inline-block;
&:before { &:before {
@ -151,9 +171,14 @@
} }
} }
&-itemArrowPlaceholder {
display: inline-block;
width: $Tree-itemArrowWidth;
}
&-itemIcon { &-itemIcon {
display: inline-block; display: inline-block;
margin-right: $gap-xs; margin-right: px2rem(3px);
} }
&-rootIcon { &-rootIcon {
@ -180,13 +205,12 @@
} }
} }
&-itemText { &-itemLabel {
user-select: none; user-select: none;
cursor: pointer;
&.is-checked, &.is-checked,
&.is-children-checked { &.is-children-checked {
color: $Tree-itemText--onChecked-color; color: $Tree-itemLabel--onChecked-color;
} }
&.is-disabled { &.is-disabled {
@ -194,7 +218,55 @@
} }
} }
&-itemText {
cursor: pointer;
}
&-placeholder { &-placeholder {
color: $text--muted-color; color: $text--muted-color;
} }
}
&-item &-item>&-itemLabel,
&-item>&-placeholder {
padding-left: $Tree-indent;
}
&-item &-item &-item>&-itemLabel,
&-item &-item>&-placeholder {
padding-left: $Tree-indent * 2;
}
&-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 3;
}
&-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 4;
}
&-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 5;
}
&-item &-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 6;
}
&-item &-item &-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 7;
}
&-item &-item &-item &-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 8;
}
&-item &-item &-item &-item &-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 9;
}
&-item &-item &-item &-item &-item &-item &-item &-item &-item &-item &-item>&-itemLabel {
padding-left: $Tree-indent * 10;
}
}

View File

@ -26,6 +26,9 @@ $borderColor: #e8ebee;
$link-onHover-decoration: none; $link-onHover-decoration: none;
$icon-color: #999;
$icon-onHover-color: $primary;
$Layout-header-boxShadow: none; $Layout-header-boxShadow: none;
$Layout-header-bg: #F5F5F5; $Layout-header-bg: #F5F5F5;
$Layout-aside-width: px2rem(180px); $Layout-aside-width: px2rem(180px);
@ -102,6 +105,10 @@ $Form-select-outer-boxShadow: px2rem(2px) px2rem(4px) px2rem(8px) rgba(0, 0, 0,
$Form-select-menu-color: #333; $Form-select-menu-color: #333;
$Form-select-menu-onHover-color: #000; $Form-select-menu-onHover-color: #000;
$Form-select-menu-onHover-bg: #eaf6fe; $Form-select-menu-onHover-bg: #eaf6fe;
$Form-select-menu-height: px2rem(24px);
$Form-select-popoverGap: px2rem(3px);
$Form-select-search-height: px2rem(30px);
$Form-selectValue-color: $primary !default;
$InputGroup-select-borderWidth: px2rem(1px); $InputGroup-select-borderWidth: px2rem(1px);
$InputGroup-select-bg: #f6f7fb; $InputGroup-select-bg: #f6f7fb;

View File

@ -20,7 +20,7 @@ interface CheckboxProps {
id?: string; id?: string;
key?: string | number; key?: string | number;
style?: React.CSSProperties; style?: React.CSSProperties;
type?: string; type: 'checkbox' | 'radio';
size?: 'sm' | 'lg' | 'small' | 'large'; size?: 'sm' | 'lg' | 'small' | 'large';
label?: string; label?: string;
labelClassName?: string; labelClassName?: string;
@ -80,19 +80,12 @@ export class Checkbox extends React.Component<CheckboxProps, any> {
labelClassName labelClassName
} = this.props; } = this.props;
className =
(className ? className : '') +
(size && sizeMap[size] ? ` ${sizeMap[size]}` : '');
return ( return (
<label <label
className={cx( className={cx(`Checkbox Checkbox--${type}`, className, {
`Checkbox Checkbox--${type}`, 'Checkbox--full': !partial,
{ [`Checkbox--${size}`]: size
'Checkbox--full': !partial })}
},
className
)}
> >
<input <input
type={type} type={type}

View File

@ -49,6 +49,9 @@ interface OverlayProps {
target?: React.ReactNode | Function; target?: React.ReactNode | Function;
} }
export default class Overlay extends React.Component<OverlayProps> { export default class Overlay extends React.Component<OverlayProps> {
static defaultProps = {
placement: 'auto'
};
constructor(props: OverlayProps) { constructor(props: OverlayProps) {
super(props as any); super(props as any);
} }

View File

@ -44,7 +44,7 @@ export class PopOver extends React.PureComponent<PopOverPorps, PopOverState> {
y: 0 y: 0
}, },
overlay: false, overlay: false,
placement: 'bottom' placement: 'auto'
}; };
state = { state = {

View File

@ -11,8 +11,8 @@ import 'react-datetime/css/react-datetime.css';
import Overlay from './Overlay'; import Overlay from './Overlay';
import PopOver from './PopOver'; import PopOver from './PopOver';
import Downshift, {ControllerStateAndHelpers} from 'downshift'; import Downshift, {ControllerStateAndHelpers} from 'downshift';
import cx from 'classnames';
import {closeIcon, Icon} from './icons'; import {closeIcon, Icon} from './icons';
// @ts-ignore
import matchSorter from 'match-sorter'; import matchSorter from 'match-sorter';
import {noop} from '../utils/helper'; import {noop} from '../utils/helper';
import find = require('lodash/find'); import find = require('lodash/find');
@ -46,6 +46,18 @@ export interface OptionProps {
clearable?: boolean; clearable?: boolean;
placeholder?: string; placeholder?: string;
autoFill?: {[propName: string]: any}; autoFill?: {[propName: string]: any};
creatable?: boolean;
onAdd?: (
idx?: number | Array<number>,
value?: any,
skipForm?: boolean
) => void;
addControls?: Array<any>;
editable?: boolean;
editControls?: Array<any>;
onEdit?: (value: Option, origin?: Option, skipForm?: boolean) => void;
removable?: boolean;
onDelete?: (value: Option) => void;
} }
export type OptionValue = string | number | null | undefined | Option; export type OptionValue = string | number | null | undefined | Option;
@ -143,11 +155,14 @@ export function normalizeOptions(
return []; return [];
} }
interface SelectProps { const DownshiftChangeTypes = Downshift.stateChangeTypes;
interface SelectProps extends OptionProps {
classPrefix: string; classPrefix: string;
classnames: ClassNamesFn; classnames: ClassNamesFn;
className?: string; className?: string;
creatable: boolean; creatable: boolean;
createBtnLabel: string;
multiple: boolean; multiple: boolean;
valueField: string; valueField: string;
labelField: string; labelField: string;
@ -167,9 +182,7 @@ interface SelectProps {
inline: boolean; inline: boolean;
disabled: boolean; disabled: boolean;
popOverContainer?: any; popOverContainer?: any;
promptTextCreator: (label: string) => string;
onChange: (value: void | string | Option | Array<Option>) => void; onChange: (value: void | string | Option | Array<Option>) => void;
onNewOptionClick: (value: Option) => void;
onFocus?: Function; onFocus?: Function;
onBlur?: Function; onBlur?: Function;
checkAll?: boolean; checkAll?: boolean;
@ -191,17 +204,16 @@ export class Select extends React.Component<SelectProps, SelectState> {
multiple: false, multiple: false,
clearable: true, clearable: true,
creatable: false, creatable: false,
createBtnLabel: '新增选项',
searchPromptText: '输入内容进行检索', searchPromptText: '输入内容进行检索',
loadingPlaceholder: '加载中..', loadingPlaceholder: '加载中..',
noResultsText: '没有结果', noResultsText: '未找到任何结果',
clearAllText: '移除所有', clearAllText: '移除所有',
clearValueText: '移除', clearValueText: '移除',
placeholder: '请选择', placeholder: '请选择',
valueField: 'value', valueField: 'value',
labelField: 'label', labelField: 'label',
spinnerClassName: 'fa fa-spinner fa-spin fa-1x fa-fw', spinnerClassName: 'fa fa-spinner fa-spin fa-1x fa-fw',
promptTextCreator: (label: string) => `新增:${label}`,
onNewOptionClick: noop,
inline: false, inline: false,
disabled: false, disabled: false,
checkAll: false, checkAll: false,
@ -229,6 +241,9 @@ export class Select extends React.Component<SelectProps, SelectState> {
this.handleKeyPress = this.handleKeyPress.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this);
this.getTarget = this.getTarget.bind(this); this.getTarget = this.getTarget.bind(this);
this.toggleCheckAll = this.toggleCheckAll.bind(this); this.toggleCheckAll = this.toggleCheckAll.bind(this);
this.handleAddClick = this.handleAddClick.bind(this);
this.handleEditClick = this.handleEditClick.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
this.state = { this.state = {
isOpen: false, isOpen: false,
@ -244,22 +259,22 @@ export class Select extends React.Component<SelectProps, SelectState> {
loadOptions, loadOptions,
options, options,
multiple, multiple,
checkAll,
defaultCheckAll, defaultCheckAll,
onChange, onChange,
simpleValue simpleValue
} = this.props; } = this.props;
let {selection} = this.state; let {selection} = this.state;
if (multiple && checkAll && defaultCheckAll && options.length) { if (multiple && defaultCheckAll && options.length) {
selection = union(options, selection); selection = union(options, selection);
this.setState( this.setState({
{ selection: selection
selection: selection });
},
() => // 因为等 State 设置完后再 onChange会让 form 再 didMount 中的
onChange(simpleValue ? selection.map(item => item.value) : selection) // onInit 出去的数据没有包含这部分,所以从 state 回调中拿出来了
); // 存在风险
onChange(simpleValue ? selection.map(item => item.value) : selection);
} }
loadOptions && loadOptions(''); loadOptions && loadOptions('');
@ -280,9 +295,13 @@ export class Select extends React.Component<SelectProps, SelectState> {
open() { open() {
this.props.disabled || this.props.disabled ||
this.setState({ this.setState(
isOpen: true {
}); isOpen: true,
highlightedIndex: -1
},
() => setTimeout(this.focus, 500)
);
} }
close() { close() {
@ -301,9 +320,13 @@ export class Select extends React.Component<SelectProps, SelectState> {
} }
this.props.disabled || this.props.disabled ||
this.setState({ this.setState(
isOpen: !this.state.isOpen {
}); isOpen: !this.state.isOpen,
highlightedIndex: -1
},
this.state.isOpen ? undefined : () => setTimeout(this.focus, 500)
);
} }
onFocus(e: any) { onFocus(e: any) {
@ -389,14 +412,9 @@ export class Select extends React.Component<SelectProps, SelectState> {
} }
handleChange(selectItem: any) { handleChange(selectItem: any) {
const {onChange, multiple, onNewOptionClick, simpleValue} = this.props; const {onChange, multiple, simpleValue} = this.props;
let {selection} = this.state; let {selection} = this.state;
if (selectItem.isNew) {
delete selectItem.isNew;
onNewOptionClick(selectItem);
}
if (multiple) { if (multiple) {
selection = selection.concat(); selection = selection.concat();
const idx = selection.indexOf(selectItem); const idx = selection.indexOf(selectItem);
@ -417,27 +435,26 @@ export class Select extends React.Component<SelectProps, SelectState> {
const loadOptions = this.props.loadOptions; const loadOptions = this.props.loadOptions;
let doLoad = false; let doLoad = false;
if (changes.isOpen !== void 0) {
update.isOpen = changes.isOpen;
}
if (changes.highlightedIndex !== void 0) {
update.highlightedIndex = changes.highlightedIndex;
}
switch (changes.type) { switch (changes.type) {
case Downshift.stateChangeTypes.keyDownEnter: case DownshiftChangeTypes.keyDownEnter:
case Downshift.stateChangeTypes.clickItem: case DownshiftChangeTypes.clickItem:
update = { update = {
...update, ...update,
inputValue: '', inputValue: '',
isOpen: multiple && checkAll ? true : false, isOpen: multiple ? true : false,
isFocused: multiple && checkAll ? true : false isFocused: multiple && checkAll ? true : false
}; };
doLoad = true; doLoad = true;
break; break;
case Downshift.stateChangeTypes.changeInput: case DownshiftChangeTypes.changeInput:
update.highlightedIndex = 0; update.highlightedIndex = 0;
case DownshiftChangeTypes.keyDownArrowDown:
case DownshiftChangeTypes.keyDownArrowUp:
case DownshiftChangeTypes.itemMouseEnter:
update = {
...update,
...changes
};
break; break;
} }
@ -462,30 +479,38 @@ export class Select extends React.Component<SelectProps, SelectState> {
onChange(''); onChange('');
} }
handleAddClick() {
const {onAdd} = this.props;
onAdd && onAdd();
}
handleEditClick(e: Event, item: any) {
const {onEdit} = this.props;
e.preventDefault();
e.stopPropagation();
onEdit && onEdit(item);
}
handleDeleteClick(e: Event, item: any) {
const {onDelete} = this.props;
e.preventDefault();
e.stopPropagation();
onDelete && onDelete(item);
}
renderValue({inputValue, isOpen}: ControllerStateAndHelpers<any>) { renderValue({inputValue, isOpen}: ControllerStateAndHelpers<any>) {
const { const {
multiple, multiple,
placeholder, placeholder,
classPrefix: ns, classPrefix: ns,
labelField, labelField,
searchable,
creatable,
disabled disabled
} = this.props; } = this.props;
const selection = this.state.selection; const selection = this.state.selection;
if (
searchable &&
!creatable &&
inputValue &&
(multiple ? !selection.length : true)
) {
return null;
}
if (!selection.length) { if (!selection.length) {
return creatable && inputValue ? null : ( return (
<div key="placeholder" className={`${ns}Select-placeholder`}> <div key="placeholder" className={`${ns}Select-placeholder`}>
{placeholder} {placeholder}
</div> </div>
@ -505,21 +530,24 @@ export class Select extends React.Component<SelectProps, SelectState> {
{item[labelField || 'label']} {item[labelField || 'label']}
</span> </span>
</div> </div>
) : inputValue && isOpen ? null : ( ) : (
<div className={`${ns}Select-value`} key={index}> <div className={`${ns}Select-value`} key={index}>
{item.label} {item[labelField || 'label']}
</div> </div>
) )
); );
} }
renderOuter({ renderOuter(
selectedItem, {
getItemProps, selectedItem,
highlightedIndex, getItemProps,
inputValue, highlightedIndex,
isOpen inputValue,
}: ControllerStateAndHelpers<any>) { isOpen
}: ControllerStateAndHelpers<any>,
getInputProps: any
) {
const { const {
popOverContainer, popOverContainer,
options, options,
@ -528,11 +556,16 @@ export class Select extends React.Component<SelectProps, SelectState> {
noResultsText, noResultsText,
loadOptions, loadOptions,
creatable, creatable,
promptTextCreator,
multiple, multiple,
classnames: cx, classnames: cx,
checkAll, checkAll,
checkAllLabel checkAllLabel,
searchable,
createBtnLabel,
disabled,
searchPromptText,
editable,
removable
} = this.props; } = this.props;
const {selection} = this.state; const {selection} = this.state;
@ -545,39 +578,41 @@ export class Select extends React.Component<SelectProps, SelectState> {
}) })
: options.concat(); : options.concat();
if (multiple) { if (multiple && checkAll) {
if (checkAll) { const optionsValues = options.map(option => option.value);
const optionsValues = options.map(option => option.value); const selectionValues = selection.map(select => select.value);
const selectionValues = selection.map(select => select.value); checkedAll = optionsValues.every(
checkedAll = optionsValues.every( option => selectionValues.indexOf(option) > -1
option => selectionValues.indexOf(option) > -1 );
); checkedPartial = optionsValues.some(
checkedPartial = optionsValues.some( option => selectionValues.indexOf(option) > -1
option => selectionValues.indexOf(option) > -1 );
);
} else {
filtedOptions = filtedOptions.filter(
(option: any) => !~selectedItem.indexOf(option)
);
}
}
if (
inputValue &&
creatable &&
!find(options, item => item[labelField || 'label'] == inputValue)
) {
filtedOptions.unshift({
[labelField]: inputValue,
[valueField]: inputValue,
isNew: true
});
} }
const menu = ( const menu = (
<div ref={this.menu} className={cx('Select-menu')}> <div ref={this.menu} className={cx('Select-menu')}>
{multiple && checkAll ? ( {searchable ? (
<div className={cx('Select-checkAll')}> <div
className={cx(`Select-input`, {
'is-focused': this.state.isFocused
})}
>
<Icon icon="search" className="icon" />
<input
{...getInputProps({
onFocus: this.onFocus,
onBlur: this.onBlur,
disabled: disabled,
placeholder: searchPromptText,
onChange: this.handleInputChange,
ref: this.inputRef
})}
/>
</div>
) : null}
{multiple && checkAll && filtedOptions.length ? (
<div className={cx('Select-option')}>
<Checkbox <Checkbox
checked={checkedPartial} checked={checkedPartial}
partial={checkedPartial && !checkedAll} partial={checkedPartial && !checkedAll}
@ -587,11 +622,12 @@ export class Select extends React.Component<SelectProps, SelectState> {
</Checkbox> </Checkbox>
</div> </div>
) : null} ) : null}
{filtedOptions.length ? ( {filtedOptions.length ? (
filtedOptions.map((item, index) => { filtedOptions.map((item, index) => {
const checked = checkAll const checked = checkAll
? selection.some((o: Option) => o.value == item.value) ? selection.some((o: Option) => o.value == item.value)
: false; : !!~selectedItem.indexOf(item);
return ( return (
<div <div
@ -609,15 +645,32 @@ export class Select extends React.Component<SelectProps, SelectState> {
(Array.isArray(selectedItem) && ~selectedItem.indexOf(item)) (Array.isArray(selectedItem) && ~selectedItem.indexOf(item))
})} })}
> >
{checkAll ? ( {removable ? (
<a data-tooltip="移除" data-position="left">
<Icon
icon="minus"
className="icon"
onClick={(e: any) => this.handleDeleteClick(e, item)}
/>
</a>
) : null}
{editable ? (
<a data-tooltip="编辑" data-position="left">
<Icon
icon="pencil"
className="icon"
onClick={(e: any) => this.handleEditClick(e, item)}
/>
</a>
) : null}
{checkAll || multiple ? (
<Checkbox <Checkbox
checked={checked} checked={checked}
trueValue={item.value} trueValue={item.value}
onChange={() => this.handleChange(item)} onChange={() => this.handleChange(item)}
> >
{item.isNew {item.disabled
? promptTextCreator(item.label as string)
: item.disabled
? item[labelField] ? item[labelField]
: highlight( : highlight(
item[labelField], item[labelField],
@ -625,8 +678,6 @@ export class Select extends React.Component<SelectProps, SelectState> {
cx('Select-option-hl') cx('Select-option-hl')
)} )}
</Checkbox> </Checkbox>
) : item.isNew ? (
promptTextCreator(item.label as string)
) : ( ) : (
<span> <span>
{item.disabled {item.disabled
@ -643,32 +694,56 @@ export class Select extends React.Component<SelectProps, SelectState> {
); );
}) })
) : ( ) : (
<div className={cx('Select-option Select-option--placeholder')}> <div className={cx('Select-noResult')}>{noResultsText}</div>
{noResultsText}
</div>
)} )}
{creatable && !disabled ? (
<a className={cx('Select-addBtn')} onClick={this.handleAddClick}>
<Icon icon="plus" className="icon" />
{createBtnLabel}
</a>
) : null}
</div> </div>
); );
if (popOverContainer) { return (
return ( <Overlay
<Overlay container={popOverContainer || this.getTarget}
container={popOverContainer} target={this.getTarget}
placement="left-bottom-left-top" show
target={this.getTarget} >
show <PopOver
overlay
className={cx('Select-popover')}
style={{width: this.target ? this.target.offsetWidth : 'auto'}}
onHide={this.close}
> >
<PopOver {menu}
className={cx('Select-popover')} </PopOver>
style={{width: this.target ? this.target.offsetWidth : 'auto'}} </Overlay>
> );
{menu}
</PopOver> // if (popOverContainer) {
</Overlay> // return (
); // <Overlay
} else { // container={popOverContainer}
return <div className={cx('Select-menuOuter')}>{menu}</div>; // placement="left-bottom-left-top"
} // target={this.getTarget}
// show
// >
// <PopOver
// overlay
// className={cx('Select-popover')}
// style={{width: this.target ? this.target.offsetWidth : 'auto'}}
// onHide={this.close}
// >
// {menu}
// </PopOver>
// </Overlay>
// );
// } else {
// return <div className={cx('Select-menuOuter')}>{menu}</div>;
// }
} }
render() { render() {
@ -697,14 +772,14 @@ export class Select extends React.Component<SelectProps, SelectState> {
inputValue={inputValue} inputValue={inputValue}
onChange={this.handleChange} onChange={this.handleChange}
onStateChange={this.handleStateChange} onStateChange={this.handleStateChange}
onOuterClick={this.close} // onOuterClick={this.close}
itemToString={item => (item ? item[labelField] : '')} itemToString={item => (item ? item[labelField] : '')}
> >
{(options: ControllerStateAndHelpers<any>) => { {(options: ControllerStateAndHelpers<any>) => {
const {isOpen, getInputProps} = options; const {isOpen, getInputProps} = options;
return ( return (
<div <div
tabIndex={searchable || disabled ? -1 : 0} tabIndex={disabled ? -1 : 0}
onKeyPress={this.handleKeyPress} onKeyPress={this.handleKeyPress}
onClick={this.toggle} onClick={this.toggle}
onFocus={this.onFocus} onFocus={this.onFocus}
@ -724,22 +799,6 @@ export class Select extends React.Component<SelectProps, SelectState> {
> >
<div className={cx(`Select-valueWrap`)}> <div className={cx(`Select-valueWrap`)}>
{this.renderValue(options)} {this.renderValue(options)}
{searchable && !disabled ? (
<input
{...getInputProps({
className: cx(`Select-input`),
onFocus: this.onFocus,
onBlur: this.onBlur,
onKeyDown: event => {
if (event.key === 'Backspace' && !inputValue) {
this.removeItem(value.length - 1);
}
},
onChange: this.handleInputChange,
ref: this.inputRef
})}
/>
) : null}
</div> </div>
{clearable && !disabled && value && value.length ? ( {clearable && !disabled && value && value.length ? (
<a onClick={this.clearValue} className={cx('Select-clear')}> <a onClick={this.clearValue} className={cx('Select-clear')}>
@ -753,7 +812,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
) : null} ) : null}
<span className={cx('Select-arrow')} /> <span className={cx('Select-arrow')} />
{isOpen ? this.renderOuter(options) : null} {isOpen ? this.renderOuter(options, getInputProps) : null}
</div> </div>
); );
}} }}

View File

@ -5,11 +5,19 @@
*/ */
import React from 'react'; import React from 'react';
import {eachTree, isVisible, isObject, autobind} from '../utils/helper'; import {
eachTree,
isVisible,
autobind,
findTreeIndex,
hasAbility,
createObject
} from '../utils/helper';
import {Option, Options, value2array} from './Checkboxes'; import {Option, Options, value2array} from './Checkboxes';
import {ClassNamesFn, themeable} from '../theme'; import {ClassNamesFn, themeable} from '../theme';
import {highlight} from '../renderers/Form/Options'; import {highlight} from '../renderers/Form/Options';
import {Icon} from './icons'; import {Icon} from './icons';
import Checkbox from './Checkbox';
interface TreeSelectorProps { interface TreeSelectorProps {
classPrefix: string; classPrefix: string;
@ -32,18 +40,19 @@ interface TreeSelectorProps {
// 多选时,选中父节点时,是否只将起子节点加入到值中。 // 多选时,选中父节点时,是否只将起子节点加入到值中。
onlyChildren?: boolean; onlyChildren?: boolean;
// 名称、取值等字段名映射 // 名称、取值等字段名映射
nameField?: string; labelField: string;
valueField?: string; valueField: string;
iconField?: string; iconField: string;
unfoldedField?: string; unfoldedField: string;
foldedField?: string; foldedField: string;
disabledField?: string; disabledField: string;
className?: string; className?: string;
itemClassName?: string; itemClassName?: string;
joinValues?: boolean; joinValues?: boolean;
extractValue?: boolean; extractValue?: boolean;
delimiter?: string; delimiter?: string;
data: Options; options: Options;
value: any; value: any;
onChange: Function; onChange: Function;
placeholder?: string; placeholder?: string;
@ -54,26 +63,31 @@ interface TreeSelectorProps {
selfDisabledAffectChildren?: boolean; selfDisabledAffectChildren?: boolean;
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
addMode?: 'dialog' | 'normal';
addable?: boolean; // 是否为内建 增、改、删。当有复杂表单的时候直接抛出去让外层能统一处理
onAdd?: Function; bultinCUD?: boolean;
openAddDialog?: Function; rootCreatable?: boolean;
editMode?: 'dialog' | 'normal'; creatable?: boolean;
onEdit?: Function; onAdd?: (
idx?: number | Array<number>,
value?: any,
skipForm?: boolean
) => void;
editable?: boolean; editable?: boolean;
openEditDialog?: Function; onEdit?: (value: Option, origin?: Option, skipForm?: boolean) => void;
removable?: boolean; removable?: boolean;
onRemove?: Function; onDelete?: (value: Option) => void;
} }
interface TreeSelectorState { interface TreeSelectorState {
value: Array<any>; value: Array<any>;
unfolded: {[propName: string]: string}; unfolded: {[propName: string]: string};
editItem: Option | null;
addItem: Option | null; inputValue: string;
addingItem: Option | null; addingParent: Option | null;
isAdding: boolean;
isEditing: boolean;
editingItem: Option | null; editingItem: Option | null;
addTop: boolean;
} }
export class TreeSelector extends React.Component< export class TreeSelector extends React.Component<
@ -89,7 +103,7 @@ export class TreeSelector extends React.Component<
disabled: false, disabled: false,
withChildren: false, withChildren: false,
onlyChildren: false, onlyChildren: false,
nameField: 'name', labelField: 'label',
valueField: 'value', valueField: 'value',
iconField: 'icon', iconField: 'icon',
unfoldedField: 'unfolded', unfoldedField: 'unfolded',
@ -115,14 +129,15 @@ export class TreeSelector extends React.Component<
multiple: props.multiple, multiple: props.multiple,
delimiter: props.delimiter, delimiter: props.delimiter,
valueField: props.valueField, valueField: props.valueField,
options: props.data options: props.options
}), }),
unfolded: this.syncUnFolded(props), unfolded: this.syncUnFolded(props),
editItem: null, // 点击编辑时的 item
addItem: null, // 点击添加时的 item inputValue: '',
addingItem: null, // 添加后的 item addingParent: null,
editingItem: null, // 编辑后的 item isAdding: false,
addTop: false // 添加一级 isEditing: false,
editingItem: null
}); });
} }
@ -131,7 +146,7 @@ export class TreeSelector extends React.Component<
if ( if (
this.props.value !== nextProps.value || this.props.value !== nextProps.value ||
this.props.data !== nextProps.data this.props.options !== nextProps.options
) { ) {
toUpdate.value = value2array(nextProps.value, { toUpdate.value = value2array(nextProps.value, {
joinValues: nextProps.joinValues, joinValues: nextProps.joinValues,
@ -139,11 +154,11 @@ export class TreeSelector extends React.Component<
multiple: nextProps.multiple, multiple: nextProps.multiple,
delimiter: nextProps.delimiter, delimiter: nextProps.delimiter,
valueField: nextProps.valueField, valueField: nextProps.valueField,
options: nextProps.data options: nextProps.options
}); });
} }
if (this.props.data !== nextProps.data) { if (this.props.options !== nextProps.options) {
toUpdate.unfolded = this.syncUnFolded(nextProps); toUpdate.unfolded = this.syncUnFolded(nextProps);
} }
@ -155,7 +170,7 @@ export class TreeSelector extends React.Component<
let unfolded: {[propName: string]: string} = {}; let unfolded: {[propName: string]: string} = {};
const {foldedField, unfoldedField} = this.props; const {foldedField, unfoldedField} = this.props;
eachTree(props.data, (node: Option, index, level) => { eachTree(props.options, (node: Option, index, level) => {
if (node.children && node.children.length) { if (node.children && node.children.length) {
let ret: any = true; let ret: any = true;
@ -179,7 +194,6 @@ export class TreeSelector extends React.Component<
@autobind @autobind
toggleUnfolded(node: any) { toggleUnfolded(node: any) {
this.setState({ this.setState({
addItem: null,
unfolded: { unfolded: {
...this.state.unfolded, ...this.state.unfolded,
[node[this.props.valueField as string]]: !this.state.unfolded[ [node[this.props.valueField as string]]: !this.state.unfolded[
@ -308,128 +322,124 @@ export class TreeSelector extends React.Component<
} }
@autobind @autobind
handleAdd(item: Option | null, isFolder: boolean) { handleAdd(parent: Option | null = null) {
const {addMode, openAddDialog, valueField} = this.props; const {bultinCUD, onAdd, options} = this.props;
let {unfolded} = this.state; let idx: Array<number> | undefined = undefined;
if (addMode === 'dialog') {
openAddDialog && openAddDialog(item ? item : null);
} else if (addMode === 'normal') {
// item 为 null 时为添加一级
if (item) {
// 添加时,默认折叠的文件夹需要展开
if (isFolder && !unfolded[item[valueField as string]]) {
unfolded = {
...unfolded,
[item[valueField as string]]: !unfolded[item[valueField as string]]
};
}
this.setState({ if (!bultinCUD) {
addItem: item, idx = parent
editItem: null, ? findTreeIndex(options, item => item === parent)
unfolded : undefined;
}); return onAdd && onAdd(idx);
} else { } else {
this.setState({ this.setState({
addTop: true, isEditing: false,
editItem: null, isAdding: true,
addItem: null addingParent: parent
}); });
}
} }
} }
@autobind @autobind
handleEdit(item: Option) { handleEdit(item: Option) {
const {editMode, openEditDialog} = this.props; const labelField = this.props.labelField;
const {addItem} = this.state; this.setState({
if (editMode === 'dialog') { isEditing: true,
openEditDialog && openEditDialog(item); isAdding: false,
addItem && editingItem: item,
this.setState({ inputValue: item[labelField]
addItem: null });
});
} else if (editMode === 'normal') {
this.setState({
editItem: item,
addItem: null
});
}
} }
@autobind @autobind
handleRemove(item: Option) { handleRemove(item: Option) {
const {onRemove} = this.props; const {onDelete} = this.props;
onRemove && onRemove(item);
onDelete && onDelete(item);
} }
@autobind @autobind
handleConfirmOnAdd() { handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const {onAdd} = this.props;
const {addItem: parent, addingItem} = this.state;
onAdd &&
onAdd({
...addingItem,
parent: parent
});
this.setState({ this.setState({
addingItem: null, inputValue: e.currentTarget.value
addItem: null,
addTop: false
}); });
} }
@autobind @autobind
handleCancelOnAdd() { handleConfirm() {
this.setState({ const {
addItem: null, inputValue: value,
addTop: false isAdding,
}); addingParent,
} editingItem,
isEditing
} = this.state;
@autobind if (!value) {
handleConfirmOnEdit() { return;
const {onEdit} = this.props; }
let {editingItem, editItem: prevItem} = this.state;
onEdit &&
onEdit({
...editingItem,
prev: prevItem
});
this.setState({
editingItem: null,
editItem: null
});
}
@autobind const {labelField, onAdd, options, onEdit} = this.props;
handleCancelOnEdit() { this.setState(
this.setState({ {
editItem: null inputValue: '',
}); isAdding: false,
} isEditing: false
},
@autobind () => {
handleChangeOnAdd(value: string) { if (isAdding && onAdd) {
this.setState({ let idx =
addingItem: { (addingParent &&
label: value findTreeIndex(options, item => item === addingParent)) ||
[];
onAdd(idx.concat(0), {[labelField]: value}, true);
} else if (isEditing && onEdit) {
onEdit(
{
...editingItem,
[labelField]: value
},
editingItem!,
true
);
}
} }
}); );
} }
@autobind @autobind
handleChangeOnEdit(item: Option, value: string) { handleCancel() {
let {editItem} = this.state;
this.setState({ this.setState({
editingItem: { inputValue: '',
...item, isAdding: false,
label: value || (editItem as Option)['label'] isEditing: false
}
}); });
} }
renderInput(prfix: JSX.Element | null = null) {
const {classnames: cx} = this.props;
const {inputValue} = this.state;
return (
<div className={cx('Tree-itemLabel')}>
<div className={cx('Tree-itemInput')}>
{prfix}
<input
onChange={this.handleInputChange}
value={inputValue}
placeholder="请输入"
/>
<a data-tooltip="取消" onClick={this.handleCancel}>
<Icon icon="close" className="icon" />
</a>
<a data-tooltip="确认" onClick={this.handleConfirm}>
<Icon icon="check" className="icon" />
</a>
</div>
</div>
);
}
@autobind @autobind
renderList( renderList(
list: Options, list: Options,
@ -442,27 +452,34 @@ export class TreeSelector extends React.Component<
showRadio, showRadio,
multiple, multiple,
disabled, disabled,
nameField = '', labelField,
valueField = '', valueField,
iconField = '', iconField,
disabledField = '', disabledField,
cascade, cascade,
selfDisabledAffectChildren, selfDisabledAffectChildren,
onlyChildren, onlyChildren,
classnames: cx, classnames: cx,
highlightTxt, highlightTxt,
data, options,
maxLength, maxLength,
minLength, minLength,
addable, creatable,
editable, editable,
removable removable
} = this.props; } = this.props;
const {addItem, editItem, unfolded, addTop, value: stateValue} = this.state; const {
unfolded,
value: stateValue,
isAdding,
addingParent,
editingItem,
isEditing
} = this.state;
let childrenChecked = 0; let childrenChecked = 0;
let ret = list.map((item, key) => { let ret = list.map((item, key) => {
if (!isVisible(item as any, data)) { if (!isVisible(item as any, options)) {
return null; return null;
} }
@ -508,25 +525,19 @@ export class TreeSelector extends React.Component<
} }
const checkbox: JSX.Element | null = multiple ? ( const checkbox: JSX.Element | null = multiple ? (
<label className={cx(`Checkbox Checkbox--checkbox Checkbox--sm`)}> <Checkbox
<input size="sm"
type="checkbox" disabled={nodeDisabled}
disabled={nodeDisabled} checked={checked}
checked={selfChecked} onChange={this.handleCheck.bind(this, item)}
onChange={e => this.handleCheck(item, e.currentTarget.checked)} />
/>
<i />
</label>
) : showRadio ? ( ) : showRadio ? (
<label className={cx(`Checkbox Checkbox--radio Checkbox--sm`)}> <Checkbox
<input size="sm"
type="radio" disabled={nodeDisabled}
disabled={nodeDisabled} checked={checked}
checked={checked} onChange={this.handleSelect.bind(this, item)}
onChange={() => this.handleSelect(item)} />
/>
<i />
</label>
) : null; ) : null;
const isLeaf = !item.children || !item.children.length; const isLeaf = !item.children || !item.children.length;
@ -538,10 +549,17 @@ export class TreeSelector extends React.Component<
'Tree-item--isLeaf': isLeaf 'Tree-item--isLeaf': isLeaf
})} })}
> >
{!editItem || {isEditing && editingItem === item ? (
(isObject(editItem) && this.renderInput(checkbox)
(editItem as Option)[valueField] !== item[valueField]) ? ( ) : (
<a> <div
className={cx('Tree-itemLabel', {
'is-children-checked':
multiple && !cascade && tmpChildrenChecked && !nodeDisabled,
'is-checked': checked,
'is-disabled': nodeDisabled
})}
>
{!isLeaf ? ( {!isLeaf ? (
<i <i
onClick={() => this.toggleUnfolded(item)} onClick={() => this.toggleUnfolded(item)}
@ -549,26 +567,14 @@ export class TreeSelector extends React.Component<
'is-folded': !unfolded[item[valueField]] 'is-folded': !unfolded[item[valueField]]
})} })}
/> />
) : null} ) : (
<span className={cx('Tree-itemArrowPlaceholder')} />
{showIcon ? ( )}
<i
className={cx(
`Tree-itemIcon ${item[iconField] ||
(childrenItems ? 'Tree-folderIcon' : 'Tree-leafIcon')}`
)}
/>
) : null}
{checkbox} {checkbox}
<span <span
className={cx('Tree-itemText', { className={cx('Tree-itemText')}
'is-children-checked':
multiple && !cascade && tmpChildrenChecked && !nodeDisabled,
'is-checked': checked,
'is-disabled': nodeDisabled
})}
onClick={() => onClick={() =>
!nodeDisabled && !nodeDisabled &&
(multiple (multiple
@ -576,81 +582,72 @@ export class TreeSelector extends React.Component<
: this.handleSelect(item)) : this.handleSelect(item))
} }
> >
{showIcon ? (
<i
className={cx(
`Tree-itemIcon ${item[iconField] ||
(childrenItems ? 'Tree-folderIcon' : 'Tree-leafIcon')}`
)}
/>
) : null}
{highlightTxt {highlightTxt
? highlight(item[nameField], highlightTxt) ? highlight(item[labelField], highlightTxt)
: item[nameField]} : item[labelField]}
</span> </span>
{!nodeDisabled && !addTop && !addItem && !editItem ? (
<span className={cx('Tree-item-icons')}> {!nodeDisabled && !isAdding && !isEditing ? (
{addable ? ( <div className={cx('Tree-item-icons')}>
<Icon {creatable && hasAbility(item, 'creatable') ? (
icon="plus" <a
className="icon" onClick={this.handleAdd.bind(this, item)}
onClick={() => this.handleAdd(item, !isLeaf)} data-tooltip="添加孩子节点"
/> >
<Icon icon="plus" className="icon" />
</a>
) : null} ) : null}
{removable ? (
<Icon {removable && hasAbility(item, 'removable') ? (
icon="minus" <a
className="icon" onClick={this.handleRemove.bind(this, item)}
onClick={() => this.handleRemove(item)} data-tooltip="移除该节点"
/> >
<Icon icon="minus" className="icon" />
</a>
) : null} ) : null}
{editable ? (
<Icon {editable && hasAbility(item, 'editable') ? (
icon="pencil" <a
className="icon" onClick={this.handleEdit.bind(this, item)}
onClick={() => this.handleEdit(item)} data-tooltip="编辑该节点"
/> >
<Icon icon="pencil" className="icon" />
</a>
) : null} ) : null}
</span> </div>
) : null} ) : null}
</a>
) : (
<div className={cx('Tree-item--isEdit')}>
<input
defaultValue={item['label']}
onChange={e =>
this.handleChangeOnEdit(item, e.currentTarget.value)
}
/>
<Icon
icon="check"
className="icon"
onClick={this.handleConfirmOnEdit}
/>
<Icon
icon="close"
className="icon"
onClick={this.handleCancelOnEdit}
/>
</div> </div>
)} )}
{/* 有children而且为展开状态 或者 添加child时 */} {/* 有children而且为展开状态 或者 添加child时 */}
{(childrenItems && unfolded[item[valueField]]) || {(childrenItems && unfolded[item[valueField]]) ||
(addItem && addItem[valueField] === item[valueField]) ? ( (isAdding && addingParent === item) ? (
<ul className={cx('Tree-sublist')}> <ul className={cx('Tree-sublist')}>
{addItem && addItem[valueField] === item[valueField] ? ( {isAdding && addingParent === item ? (
<li> <li className={cx('Tree-item')}>
<input {this.renderInput(
onChange={e => checkbox
this.handleChangeOnAdd(e.currentTarget.value) ? React.cloneElement(checkbox, {
} checked: false,
/> disabled: true
<Icon })
icon="check" : null
className="icon" )}
onClick={this.handleConfirmOnAdd}
/>
<Icon
icon="close"
className="icon"
onClick={this.handleCancelOnAdd}
/>
</li> </li>
) : null} ) : null}
{childrenItems} {childrenItems}
</ul> </ul>
) : !childrenItems && item.placeholder ? (
<div className={cx('Tree-placeholder')}>{item.placeholder}</div>
) : null} ) : null}
</li> </li>
); );
@ -670,68 +667,76 @@ export class TreeSelector extends React.Component<
rootLabel, rootLabel,
showIcon, showIcon,
classnames: cx, classnames: cx,
addable creatable,
rootCreatable,
disabled
} = this.props; } = this.props;
let data = this.props.data; let options = this.props.options;
const {value, addTop} = this.state; const {value, isAdding, addingParent, isEditing, inputValue} = this.state;
let addBtn = null;
if (creatable && rootCreatable !== false && hideRoot) {
addBtn = (
<a
className={cx('Tree-addTopBtn', {
'is-disabled': isAdding || isEditing
})}
onClick={this.handleAdd.bind(this, null)}
>
<Icon icon="plus" className="icon" />
<span></span>
</a>
);
}
return ( return (
<div className={cx(`Tree ${className || ''}`)}> <div className={cx(`Tree ${className || ''}`)}>
{data && data.length ? ( {options && options.length ? (
<ul className={cx('Tree-list')}> <ul className={cx('Tree-list')}>
{hideRoot ? ( {hideRoot ? (
this.renderList(data, value, false).dom <>
) : ( {addBtn}
<li className={cx('Tree-item Tree-rootItem')}> {isAdding && !addingParent ? (
<a> <li className={cx('Tree-item')}>{this.renderInput()}</li>
{showIcon ? (
<i className={cx('Tree-itemIcon Tree-rootIcon')} />
) : null}
<label
className={cx('Tree-itemLabel', {
'is-checked': !value || !value.length
})}
>
<span
className={cx('Tree-itemText')}
onClick={this.clearSelect}
>
{rootLabel}
</span>
</label>
</a>
{addable ? (
<div className={cx('Tree-addTop')}>
{!addTop ? (
<p onClick={() => this.handleAdd(null, false)}>
<Icon icon="plus" className="icon" />
<span></span>
</p>
) : null}
{addTop ? (
<div className={cx('Tree-addTop-input')}>
<input
onChange={e =>
this.handleChangeOnAdd(e.currentTarget.value)
}
/>
<Icon
icon="check"
className="icon"
onClick={this.handleConfirmOnAdd}
/>
<Icon
icon="close"
className="icon"
onClick={this.handleCancelOnAdd}
/>
</div>
) : null}
</div>
) : null} ) : null}
{this.renderList(options, value, false).dom}
</>
) : (
<li
className={cx('Tree-rootItem', {
'is-checked': !value || !value.length
})}
>
<div className={cx('Tree-itemLabel')}>
<span className={cx('Tree-itemText')}>
{showIcon ? (
<i className={cx('Tree-itemIcon Tree-rootIcon')} />
) : null}
{rootLabel}
</span>
{!disabled &&
creatable &&
rootCreatable !== false &&
!isAdding &&
!isEditing ? (
<div className={cx('Tree-item-icons')}>
{creatable ? (
<a
onClick={this.handleAdd.bind(this, null)}
data-tooltip="添加一级节点"
>
<Icon icon="plus" className="icon" />
</a>
) : null}
</div>
) : null}
</div>
<ul className={cx('Tree-sublist')}> <ul className={cx('Tree-sublist')}>
{this.renderList(data, value, false).dom} {isAdding && !addingParent ? (
<li className={cx('Tree-item')}>{this.renderInput()}</li>
) : null}
{this.renderList(options, value, false).dom}
</ul> </ul>
</li> </li>
)} )}

View File

@ -48,6 +48,9 @@ import SuccessIcon from '../icons/success.svg';
// @ts-ignore // @ts-ignore
import FailIcon from '../icons/fail.svg'; import FailIcon from '../icons/fail.svg';
// @ts-ignore
import SearchIcon from '../icons/search.svg';
// 兼容原来的用法,后续不直接试用。 // 兼容原来的用法,后续不直接试用。
// @ts-ignore // @ts-ignore
export const closeIcon = <CloseIcon />; export const closeIcon = <CloseIcon />;
@ -103,6 +106,7 @@ registerIcon('upload', UploadIcon);
registerIcon('file', FileIcon); registerIcon('file', FileIcon);
registerIcon('success', SuccessIcon); registerIcon('success', SuccessIcon);
registerIcon('fail', FailIcon); registerIcon('fail', FailIcon);
registerIcon('search', SearchIcon);
export function Icon({ export function Icon({
icon, icon,

View File

@ -1,3 +1,9 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3506" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg viewBox="0 0 13 9" version="1.1"
<path d="M972.544 175.189333a31.658667 31.658667 0 0 1 15.701333 5.162667 31.872 31.872 0 0 1 7.296 46.976c-0.682667 0.896-1.493333 1.664-2.218666 2.474667L343.082667 876.032a32.341333 32.341333 0 0 1-37.546667 5.589333 37.504 37.504 0 0 1-8.064-6.101333L30.208 597.845333c-0.768-0.810667-1.536-1.621333-2.218667-2.517333a32.256 32.256 0 0 1 14.378667-49.749333 32 32 0 0 1 28.8 3.584c2.474667 1.664 2.944 2.304 5.12 4.309333l244.736 254.250667L948.181333 184.448l2.517334-2.261333a36.693333 36.693333 0 0 1 11.861333-6.016c2.901333-0.725333 3.669333-0.725333 6.613333-1.024l3.370667 0.042666z" p-id="3507"></path> xmlns="http://www.w3.org/2000/svg" p-id="3506"
</svg> xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(6.656854, 2.656854) scale(-1, 1) rotate(-315.000000) translate(-6.656854, -2.656854) ">
<polygon id="path-1" points="11.1568542 5.15685425 11.1568542 -0.843145751 12.1568542 -0.843145751 12.1568542 6.15685425 1.15685425 6.15685425 1.15685425 5.15685425"></polygon>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 458 B

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024" version="1.1" p-id="1463"> xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12 12" version="1.1">
<path d="M967.81435 106.836237 917.16274 56.18565 512 461.34839 106.836237 56.18565 56.184627 106.836237 461.34839 512 56.184627 917.163763 106.836237 967.815373 512 562.65161 917.16274 967.815373 967.81435 917.163763 562.650587 512Z" p-id="1464" data-spm-anchor-id="a313x.7781069.0.i0" /> <polygon id="path-1" points="6.0003653 5.2970518 10.5993691 0.6980479600000002 11.3064759 1.4051547400000004 6.7074721 6.0041586 11.3009516 10.5976381 10.5938448 11.3047449 6.0003653 6.7112654 1.4056713299999997 11.3059593 0.6985645500000004 10.5988525 5.2932585 6.0041586 0.6956119200000002 1.4065120000000002 1.4027187000000003 0.69940522"></polygon>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 486 B

4
src/icons/search.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 18 18" version="1.1">
<path d="M2,8 C2,4.691 4.691,2 8,2 C11.309,2 14,4.691 14,8 C14,11.309 11.309,14 8,14 C4.691,14 2,11.309 2,8 L2,8 Z M18,16.586 L14.314,12.9 C15.367,11.545 16,9.849 16,8 C16,3.582 12.418,0 8,0 C3.582,0 0,3.582 0,8 C0,12.418 3.582,16 8,16 C9.849,16 11.545,15.367 12.9,14.314 L16.586,18 L18,16.586 Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -135,7 +135,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
control: any; control: any;
lastQuery: any; lastQuery: any;
dataInvalid: boolean = false; dataInvalid: boolean = false;
timer: number; timer: NodeJS.Timeout;
mounted: boolean; mounted: boolean;
constructor(props: CRUDProps) { constructor(props: CRUDProps) {
super(props); super(props);
@ -331,7 +331,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
env.jumpTo(filter(action.redirect, data), action); env.jumpTo(filter(action.redirect, data), action);
return store return store
.saveRemote(action.api, data, { .saveRemote(action.api!, data, {
successMessage: successMessage:
(action.messages && action.messages.success) || (action.messages && action.messages.success) ||
(messages && messages.saveSuccess), (messages && messages.saveSuccess),

View File

@ -118,16 +118,7 @@ export default class DropDownButton extends React.Component<
if (popOverContainer) { if (popOverContainer) {
return ( return (
<Overlay <Overlay container={popOverContainer} target={() => this.target} show>
container={popOverContainer}
placement={
align === 'right'
? 'right-bottom-right-top'
: 'left-bottom-left-top'
}
target={() => this.target}
show
>
<PopOver <PopOver
overlay overlay
onHide={this.close} onHide={this.close}

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {IFormStore, IFormItemStore} from '../../store/form'; import {IFormStore, IFormItemStore} from '../../store/form';
import debouce = require('lodash/debounce'); import debouce = require('lodash/debounce');

View File

@ -10,9 +10,10 @@ import {
RendererConfig, RendererConfig,
HocStoreFactory HocStoreFactory
} from '../../factory'; } from '../../factory';
import {anyChanged, ucFirst, getWidthRate} from '../../utils/helper'; import {anyChanged, ucFirst, getWidthRate, autobind} from '../../utils/helper';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {FormHorizontal, FormSchema} from '.'; import {FormHorizontal, FormSchema} from '.';
import {Schema} from '../../types';
export interface FormItemBasicConfig extends Partial<RendererConfig> { export interface FormItemBasicConfig extends Partial<RendererConfig> {
type?: string; type?: string;
@ -33,13 +34,11 @@ export interface FormItemBasicConfig extends Partial<RendererConfig> {
validate?: (values: any, value: any) => string | boolean; validate?: (values: any, value: any) => string | boolean;
} }
export interface FormItemState { // 自己接收到属性。
isFocused: boolean;
}
export interface FormItemProps extends RendererProps { export interface FormItemProps extends RendererProps {
name?: string; name?: string;
formStore?: IFormStore; formStore?: IFormStore;
formItem?: IFormItemStore;
formInited: boolean; formInited: boolean;
formMode: 'normal' | 'horizontal' | 'inline' | 'row' | 'default'; formMode: 'normal' | 'horizontal' | 'inline' | 'row' | 'default';
formHorizontal: FormHorizontal; formHorizontal: FormHorizontal;
@ -56,7 +55,7 @@ export interface FormItemProps extends RendererProps {
values: {[propName: string]: any}, values: {[propName: string]: any},
submitOnChange?: boolean submitOnChange?: boolean
) => void; ) => void;
addHook: (fn: Function, mode?: 'validate' | 'init') => void; addHook: (fn: Function, mode?: 'validate' | 'init') => () => void;
removeHook: (fn: Function, mode?: 'validate' | 'init') => void; removeHook: (fn: Function, mode?: 'validate' | 'init') => void;
renderFormItems: ( renderFormItems: (
schema: FormSchema, schema: FormSchema,
@ -89,8 +88,10 @@ export interface FormItemProps extends RendererProps {
error?: string; error?: string;
} }
export type FormControlProps = RendererProps & // 下发下去的属性
Exclude< export type FormControlProps = RendererProps & {
onOpenDialog: (schema: Schema, data: any) => Promise<any>;
} & Exclude<
FormItemProps, FormItemProps,
| 'inputClassName' | 'inputClassName'
| 'renderControl' | 'renderControl'
@ -114,29 +115,15 @@ export interface FormItemConfig extends FormItemBasicConfig {
component: FormControlComponent; component: FormControlComponent;
} }
export class FormItemWrap extends React.Component< export class FormItemWrap extends React.Component<FormItemProps> {
FormItemProps,
FormItemState
> {
reaction: any; reaction: any;
constructor(props: FormItemProps) {
super(props);
this.state = {
isFocused: false
};
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
componentWillMount() { componentWillMount() {
const {formItem: model} = this.props; const {formItem: model} = this.props;
if (model) { if (model) {
this.reaction = reaction( this.reaction = reaction(
() => model.errors.join(''), () => `${model.errors.join('')}${model.isFocused}${model.dialogOpen}`,
() => this.forceUpdate() () => this.forceUpdate()
); );
} }
@ -146,20 +133,51 @@ export class FormItemWrap extends React.Component<
this.reaction && this.reaction(); this.reaction && this.reaction();
} }
@autobind
handleFocus(e: any) { handleFocus(e: any) {
this.setState({ const {formItem: model} = this.props;
isFocused: true model && model.focus();
});
this.props.onFocus && this.props.onFocus(e); this.props.onFocus && this.props.onFocus(e);
} }
@autobind
handleBlur(e: any) { handleBlur(e: any) {
this.setState({ const {formItem: model} = this.props;
isFocused: false model && model.blur();
});
this.props.onBlur && this.props.onBlur(e); this.props.onBlur && this.props.onBlur(e);
} }
@autobind
async handleOpenDialog(schema: Schema, data: any) {
const {formItem: model} = this.props;
if (!model) {
return;
}
return new Promise(resolve =>
model.openDialog(schema, data, (result?: any) => resolve(result))
);
}
@autobind
handleDialogConfirm([values]: Array<any>) {
const {formItem: model} = this.props;
if (!model) {
return;
}
model.closeDialog(values);
}
@autobind
handleDialogClose() {
const {formItem: model} = this.props;
if (!model) {
return;
}
model.closeDialog();
}
renderControl() { renderControl() {
const { const {
inputClassName, inputClassName,
@ -179,6 +197,7 @@ export class FormItemWrap extends React.Component<
const controlSize = size || defaultSize; const controlSize = size || defaultSize;
return renderControl({ return renderControl({
...rest, ...rest,
onOpenDialog: this.handleOpenDialog,
type, type,
classnames: cx, classnames: cx,
formItem: model, formItem: model,
@ -299,7 +318,7 @@ export class FormItemWrap extends React.Component<
}) })
: null} : null}
{hint && this.state.isFocused {hint && model && model.isFocused
? render('hint', hint, { ? render('hint', hint, {
className: cx(`Form-hint`) className: cx(`Form-hint`)
}) })
@ -395,7 +414,7 @@ export class FormItemWrap extends React.Component<
}) })
: null} : null}
{hint && this.state.isFocused {hint && model && model.isFocused
? render('hint', hint, { ? render('hint', hint, {
className: cx(`Form-hint`) className: cx(`Form-hint`)
}) })
@ -490,7 +509,7 @@ export class FormItemWrap extends React.Component<
}) })
: null} : null}
{hint && this.state.isFocused {hint && model && model.isFocused
? render('hint', hint, { ? render('hint', hint, {
className: cx(`Form-hint`) className: cx(`Form-hint`)
}) })
@ -588,7 +607,7 @@ export class FormItemWrap extends React.Component<
: null} : null}
</div> </div>
{hint && this.state.isFocused {hint && model && model.isFocused
? render('hint', hint, { ? render('hint', hint, {
className: cx(`Form-hint`) className: cx(`Form-hint`)
}) })
@ -612,19 +631,38 @@ export class FormItemWrap extends React.Component<
} }
render() { render() {
const {formMode, inputOnly, wrap} = this.props; const {formMode, inputOnly, wrap, render, formItem: model} = this.props;
if (wrap === false || inputOnly) { if (wrap === false || inputOnly) {
return this.renderControl(); return this.renderControl();
} }
return formMode === 'inline' return (
? this.renderInline() <>
: formMode === 'horizontal' {formMode === 'inline'
? this.renderHorizontal() ? this.renderInline()
: formMode === 'row' : formMode === 'horizontal'
? this.renderRow() ? this.renderHorizontal()
: this.renderNormal(); : formMode === 'row'
? this.renderRow()
: this.renderNormal()}
{model
? render(
'modal',
{
type: 'dialog',
...model.dialogSchema
},
{
show: model.dialogOpen,
onClose: this.handleDialogClose,
onConfirm: this.handleDialogConfirm,
data: model.dialogData
}
)
: null}
</>
);
} }
} }
@ -796,6 +834,7 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
return ( return (
<Control <Control
{...rest} {...rest}
onOpenDialog={this.handleOpenDialog}
size={config.sizeMutable !== false ? undefined : size} size={config.sizeMutable !== false ? undefined : size}
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} onBlur={this.handleBlur}

View File

@ -8,6 +8,7 @@ import cx from 'classnames';
import {FormControlProps, FormItem} from './Item'; import {FormControlProps, FormItem} from './Item';
import {buildApi, isValidApi, isEffectiveApi} from '../../utils/api'; import {buildApi, isValidApi, isEffectiveApi} from '../../utils/api';
import {Checkbox, Spinner} from '../../components'; import {Checkbox, Spinner} from '../../components';
import {autobind, setVariable} from '../../utils/helper';
export interface Column { export interface Column {
label: string; label: string;
@ -50,6 +51,8 @@ export default class MatrixCheckbox extends React.Component<
state: MatrixState; state: MatrixState;
sourceInvalid: boolean = false; sourceInvalid: boolean = false;
mounted: boolean = false;
constructor(props: MatrixProps) { constructor(props: MatrixProps) {
super(props); super(props);
@ -61,12 +64,17 @@ export default class MatrixCheckbox extends React.Component<
this.toggleItem = this.toggleItem.bind(this); this.toggleItem = this.toggleItem.bind(this);
this.reload = this.reload.bind(this); this.reload = this.reload.bind(this);
this.initOptions = this.initOptions.bind(this);
}
componentWillMount() {
this.mounted = true;
} }
componentDidMount() { componentDidMount() {
const {formInited, addHook} = this.props; const {formInited, addHook} = this.props;
formInited ? this.reload() : addHook(this.reload, 'init'); formInited ? this.reload() : addHook(this.initOptions, 'init');
} }
componentWillReceiveProps(nextProps: MatrixProps) { componentWillReceiveProps(nextProps: MatrixProps) {
@ -104,7 +112,24 @@ export default class MatrixCheckbox extends React.Component<
} }
} }
reload() { componentWillUnmount() {
this.mounted = false;
const {removeHook} = this.props;
removeHook(this.initOptions, 'init');
}
async initOptions(data: any) {
await this.reload();
const {formItem, name} = this.props;
if (!formItem) {
return;
}
if (formItem.value) {
setVariable(data, name!, formItem.value);
}
}
async reload() {
const {source, data, env, onChange} = this.props; const {source, data, env, onChange} = this.props;
if (!isEffectiveApi(source, data) || this.state.loading) { if (!isEffectiveApi(source, data) || this.state.loading) {
@ -115,45 +140,61 @@ export default class MatrixCheckbox extends React.Component<
throw new Error('fetcher is required'); throw new Error('fetcher is required');
} }
// 需要联动加载吗?我看不一定会用到,先这样吧。 // todo 优化这块
this.setState( return await new Promise((resolve, reject) => {
{ if (!this.mounted) {
loading: true return resolve();
},
() => {
env
.fetcher(source, data)
.then(ret => {
if (!ret.ok) {
throw new Error(ret.msg || '数据请求错误');
}
this.setState(
{
loading: false,
rows: (ret.data as any).rows || [],
columns: (ret.data as any).columns || []
},
() => {
let value = (ret.data as any).value;
if (value) {
value = mergeValue(
value,
this.state.columns,
this.state.rows
);
onChange(value);
}
}
);
})
.catch(reason =>
this.setState({
error: reason,
loading: false
})
);
} }
);
this.setState(
{
loading: true
},
() => {
if (!this.mounted) {
return resolve();
}
env
.fetcher(source, data)
.then(ret => {
if (!ret.ok) {
throw new Error(ret.msg || '数据请求错误');
}
if (!this.mounted) {
return resolve();
}
this.setState(
{
loading: false,
rows: (ret.data as any).rows || [],
columns: (ret.data as any).columns || []
},
() => {
let value = (ret.data as any).value;
if (value) {
value = mergeValue(
value,
this.state.columns,
this.state.rows
);
onChange(value);
}
resolve();
}
);
})
.catch(reason =>
this.setState(
{
error: reason,
loading: false
},
resolve
)
);
}
);
});
} }
toggleItem(checked: boolean, x: number, y: number) { toggleItem(checked: boolean, x: number, y: number) {

View File

@ -308,12 +308,7 @@ export default class NestedSelectControl extends React.Component<
if (popOverContainer) { if (popOverContainer) {
return ( return (
<Overlay <Overlay container={popOverContainer} target={() => this.target} show>
container={popOverContainer}
placement="left-bottom-left-top right-bottom-right-top"
target={() => this.target}
show
>
<PopOver <PopOver
className={cx('NestedSelect-popover')} className={cx('NestedSelect-popover')}
style={{minWidth: this.target.offsetWidth}} style={{minWidth: this.target.offsetWidth}}

View File

@ -1,11 +1,18 @@
/**
* @file SelectRadiosCheckboxes
* ListButtonGroup
*/
import {Api, Schema} from '../../types'; import {Api, Schema} from '../../types';
import {isEffectiveApi, isApiOutdated} from '../../utils/api';
import { import {
buildApi, anyChanged,
isEffectiveApi, autobind,
isValidApi, createObject,
isApiOutdated setVariable,
} from '../../utils/api'; spliceTree,
import {anyChanged, autobind} from '../../utils/helper'; findTreeIndex,
getTree
} from '../../utils/helper';
import {reaction} from 'mobx'; import {reaction} from 'mobx';
import {FormControlProps, registerFormItem, FormItemBasicConfig} from './Item'; import {FormControlProps, registerFormItem, FormItemBasicConfig} from './Item';
import {IFormItemStore} from '../../store/formItem'; import {IFormItemStore} from '../../store/formItem';
@ -13,8 +20,9 @@ export type OptionsControlComponent = React.ComponentType<FormControlProps>;
import React from 'react'; import React from 'react';
import {resolveVariableAndFilter} from '../../utils/tpl-builtin'; import {resolveVariableAndFilter} from '../../utils/tpl-builtin';
import {evalExpression} from '../../utils/tpl';
import {Option, OptionProps, normalizeOptions} from '../../components/Select'; import {Option, OptionProps, normalizeOptions} from '../../components/Select';
import {filter} from '../../utils/tpl';
import findIndex from 'lodash/findIndex';
export {Option}; export {Option};
@ -26,6 +34,7 @@ export interface OptionsConfig extends OptionsBasicConfig {
component: React.ComponentType<OptionsControlProps>; component: React.ComponentType<OptionsControlProps>;
} }
// 下发给注册进来的组件的属性。
export interface OptionsControlProps extends FormControlProps, OptionProps { export interface OptionsControlProps extends FormControlProps, OptionProps {
source?: Api; source?: Api;
name?: string; name?: string;
@ -35,30 +44,36 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
setOptions: (value: Array<any>) => void; setOptions: (value: Array<any>) => void;
setLoading: (value: boolean) => void; setLoading: (value: boolean) => void;
reloadOptions: () => void; reloadOptions: () => void;
addable?: boolean; creatable?: boolean;
onAdd?: () => void; onAdd?: (
idx?: number | Array<number>,
value?: any,
skipForm?: boolean
) => void;
addControls?: Array<any>;
editable?: boolean; editable?: boolean;
onEdit?: (value: Option) => void; editControls?: Array<any>;
onEdit?: (value: Option, origin?: Option, skipForm?: boolean) => void;
removable?: boolean; removable?: boolean;
onDelete?: (value: Option) => void; onDelete?: (value: Option) => void;
} }
// 自己接收的属性。
export interface OptionsProps extends FormControlProps, OptionProps { export interface OptionsProps extends FormControlProps, OptionProps {
sourcce?: Api; source?: Api;
creatable?: boolean;
addApi?: Api; addApi?: Api;
addMode?: 'dialog' | 'normal'; addControls?: Array<any>;
addDialog?: Schema;
editApi?: Api; editApi?: Api;
editMode?: 'dialog' | 'normal'; editControls?: Array<any>;
editDialog?: Schema;
deleteApi?: Api; deleteApi?: Api;
deleteConfirmText?: string; deleteConfirmText?: string;
optionLabel?: string;
} }
export function registerOptionsControl(config: OptionsConfig) { export function registerOptionsControl(config: OptionsConfig) {
const Control = config.component; const Control = config.component;
// @observer
class FormOptionsItem extends React.Component<OptionsProps, any> { class FormOptionsItem extends React.Component<OptionsProps, any> {
static displayName = `OptionsControl(${config.type})`; static displayName = `OptionsControl(${config.type})`;
static defaultProps = { static defaultProps = {
@ -70,6 +85,7 @@ export function registerOptionsControl(config: OptionsConfig) {
multiple: false, multiple: false,
placeholder: '请选择', placeholder: '请选择',
resetValue: '', resetValue: '',
deleteConfirmText: '确定要删除?',
...Control.defaultProps ...Control.defaultProps
}; };
static propsList: any = (Control as any).propsList static propsList: any = (Control as any).propsList
@ -113,10 +129,10 @@ export function registerOptionsControl(config: OptionsConfig) {
let loadOptions: boolean = initFetch !== false; let loadOptions: boolean = initFetch !== false;
if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(source) && formItem) { if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(source as string) && formItem) {
formItem.setOptions( formItem.setOptions(
normalizeOptions( normalizeOptions(
resolveVariableAndFilter(source, data, '| raw') || [] resolveVariableAndFilter(source as string, data, '| raw') || []
) )
); );
loadOptions = false; loadOptions = false;
@ -134,7 +150,9 @@ export function registerOptionsControl(config: OptionsConfig) {
} }
loadOptions && loadOptions &&
(formInited ? this.reload() : addHook && addHook(this.reload, 'init')); (formInited
? this.reload()
: addHook && addHook(this.initOptions, 'init'));
} }
componentDidMount() { componentDidMount() {
@ -207,7 +225,7 @@ export function registerOptionsControl(config: OptionsConfig) {
) { ) {
if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(props.source as string)) { if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(props.source as string)) {
const prevOptions = resolveVariableAndFilter( const prevOptions = resolveVariableAndFilter(
prevProps.source, prevProps.source as string,
prevProps.data, prevProps.data,
'| raw' '| raw'
); );
@ -417,6 +435,18 @@ export function registerOptionsControl(config: OptionsConfig) {
return formItem.loadOptions(source, data, undefined, false, onChange); return formItem.loadOptions(source, data, undefined, false, onChange);
} }
@autobind
async initOptions(data: any) {
await this.reload();
const {formItem, name} = this.props;
if (!formItem) {
return;
}
if (formItem.value) {
setVariable(data, name!, formItem.value);
}
}
focus() { focus() {
this.input && this.input.focus && this.input.focus(); this.input && this.input.focus && this.input.focus();
} }
@ -439,8 +469,280 @@ export function registerOptionsControl(config: OptionsConfig) {
formItem && formItem.setLoading(value); formItem && formItem.setLoading(value);
} }
@autobind
async handleOptionAdd(
idx: number | Array<number> = -1,
value: any,
skipForm: boolean = false
) {
let {
addControls,
disabled,
labelField,
onOpenDialog,
optionLabel,
addApi,
source,
data,
valueField,
formItem: model,
createBtnLabel,
env
} = this.props;
// 禁用或者没有配置 name
if (disabled || !model) {
return;
}
// 用户没有配置表单项,则自动创建一个 label 输入
if (!skipForm && (!Array.isArray(addControls) || !addControls.length)) {
addControls = [
{
type: 'text',
name: labelField || 'label',
label: false,
placeholder: '请输入名称'
}
];
}
const ctx = createObject(
data,
Array.isArray(idx)
? {
parent: getTree(model.options, idx.slice(0, idx.length - 1)),
...value
}
: value
);
let result: any = skipForm
? ctx
: await onOpenDialog(
{
type: 'dialog',
title: createBtnLabel || `新增${optionLabel || '选项'}`,
body: {
type: 'form',
api: addApi,
controls: addControls
}
},
ctx
);
// 单独发请求
if (skipForm && addApi) {
try {
const payload = await env.fetcher(addApi!, result, {
method: 'post'
});
if (!payload.ok) {
env.notify('error', payload.msg || '新增失败,请仔细检查');
} else {
result = payload.data || result;
}
} catch (e) {
result = null;
console.error(e);
env.notify('error', e.message);
}
}
// 有 result 说明弹框点了确认。否则就是取消了。
if (!result) {
return;
}
// 没走服务端的。
if (!result.__saved) {
result = {
...result,
[valueField || 'value']: result[labelField || 'label']
};
}
// 如果配置了 source 直接重新拉取接口就够了
if (source) {
this.reload();
} else {
// 否则直接前端变更 options
let options = model.options.concat();
if (Array.isArray(idx)) {
options = spliceTree(options, idx, 0, {...result});
} else {
~idx
? options.splice(idx, 0, {...result})
: options.push({...result});
}
model.setOptions(options);
}
}
@autobind
async handleOptionEdit(
value: any,
origin: any = value,
skipForm: boolean = false
) {
let {
editControls,
disabled,
labelField,
onOpenDialog,
editApi,
env,
source,
data,
formItem: model,
optionLabel
} = this.props;
if (disabled || !model) {
return;
}
if (!skipForm && (!Array.isArray(editControls) || !editControls.length)) {
editControls = [
{
type: 'text',
name: labelField || 'label',
label: false,
placeholder: '请输入名称'
}
];
}
let result = skipForm
? value
: await onOpenDialog(
{
type: 'dialog',
title: `编辑${optionLabel || '选项'}`,
body: {
type: 'form',
api: editApi,
controls: editControls
}
},
createObject(data, value)
);
// 单独发请求
if (skipForm && editApi) {
try {
const payload = await env.fetcher(
editApi!,
createObject(data, result),
{
method: 'post'
}
);
if (!payload.ok) {
env.notify('error', payload.msg || '保存失败,请仔细检查');
} else {
result = payload.data || result;
}
} catch (e) {
result = null;
console.error(e);
env.notify('error', e.message);
}
}
// 没有结果,说明取消了。
if (!result) {
return;
}
if (source) {
this.reload();
} else {
const indexes = findTreeIndex(model.options, item => item === origin);
if (indexes) {
model.setOptions(
spliceTree(model.options, indexes, 1, {
...origin,
...result
})
);
}
}
}
@autobind
async handleOptionDelete(value: any) {
let {
deleteConfirmText,
disabled,
data,
deleteApi,
env,
formItem: model,
source,
valueField
} = this.props;
if (disabled || !model) {
return;
}
const ctx = createObject(data, value);
// 如果配置了 deleteConfirmText 让用户先确认。
const confirmed = deleteConfirmText
? await env.confirm(filter(deleteConfirmText, ctx))
: true;
if (!confirmed) {
return;
}
// 通过 deleteApi 删除。
try {
if (!deleteApi) {
throw new Error('请配置 deleteApi');
}
const result = await env.fetcher(deleteApi!, ctx, {
method: 'delete'
});
if (!result.ok) {
env.notify('error', result.msg || '删除失败,请重试');
} else if (source) {
this.reload();
} else {
const options = model.options.concat();
const idx = findIndex(
options,
item => item[valueField || 'value'] == value[valueField || 'value']
);
if (~idx) {
options.splice(idx, 1);
model.setOptions(options);
}
}
} catch (e) {
console.error(e);
env.notify('error', e.message);
}
}
render() { render() {
const {value, formItem} = this.props; const {
value,
formItem,
addApi,
editApi,
deleteApi,
creatable,
editable,
removable
} = this.props;
return ( return (
<Control <Control
@ -455,6 +757,12 @@ export function registerOptionsControl(config: OptionsConfig) {
setOptions={this.setOptions} setOptions={this.setOptions}
syncOptions={this.syncOptions} syncOptions={this.syncOptions}
reloadOptions={this.reload} reloadOptions={this.reload}
creatable={creatable || isEffectiveApi(addApi)}
editable={editable || isEffectiveApi(editApi)}
removable={removable || isEffectiveApi(deleteApi)}
onAdd={this.handleOptionAdd}
onEdit={this.handleOptionEdit}
onDelete={this.handleOptionDelete}
/> />
); );
} }

View File

@ -33,7 +33,6 @@ export default class SelectControl extends React.Component<SelectProps, any> {
leading: false leading: false
}); });
this.inputRef = this.inputRef.bind(this); this.inputRef = this.inputRef.bind(this);
this.handleNewOptionClick = this.handleNewOptionClick.bind(this);
} }
inputRef(ref: any) { inputRef(ref: any) {
@ -51,6 +50,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
delimiter, delimiter,
multiple, multiple,
type, type,
valueField,
onChange, onChange,
setOptions, setOptions,
options, options,
@ -63,7 +63,11 @@ export default class SelectControl extends React.Component<SelectProps, any> {
(Array.isArray(value) ? value : value ? [value] : []).forEach( (Array.isArray(value) ? value : value ? [value] : []).forEach(
(option: any) => { (option: any) => {
let resolved = find(options, (item: any) => item.value == option.value); let resolved = find(
options,
(item: any) =>
item[valueField || 'value'] == option[valueField || 'value']
);
resolved || additonalOptions.push(option); resolved || additonalOptions.push(option);
} }
); );
@ -71,22 +75,24 @@ export default class SelectControl extends React.Component<SelectProps, any> {
if (joinValues) { if (joinValues) {
if (multiple) { if (multiple) {
newValue = Array.isArray(value) newValue = Array.isArray(value)
? (value.map(item => item.value).join(delimiter) as string) ? (value
.map(item => item[valueField || 'value'])
.join(delimiter) as string)
: value : value
? (value as Option).value ? (value as Option)[valueField || 'value']
: ''; : '';
} else { } else {
newValue = newValue ? (newValue as Option).value : ''; newValue = newValue ? (newValue as Option)[valueField || 'value'] : '';
} }
} else if (extractValue) { } else if (extractValue) {
if (multiple) { if (multiple) {
newValue = Array.isArray(value) newValue = Array.isArray(value)
? value.map(item => item.value) ? value.map(item => item[valueField || 'value'])
: value : value
? [(value as Option).value] ? [(value as Option)[valueField || 'value']]
: ['']; : [''];
} else { } else {
newValue = newValue ? (newValue as Option).value : ''; newValue = newValue ? (newValue as Option)[valueField || 'value'] : '';
} }
} }
@ -161,16 +167,6 @@ export default class SelectControl extends React.Component<SelectProps, any> {
return combinedOptions; return combinedOptions;
} }
handleNewOptionClick(option: any) {
const {setOptions, options} = this.props;
let mergedOptions: Array<any> = options.concat();
mergedOptions.push({
...option
});
setOptions(mergedOptions);
}
reload() { reload() {
const reload = this.props.reloadOptions; const reload = this.props.reloadOptions;
reload && reload(); reload && reload();
@ -211,12 +207,11 @@ export default class SelectControl extends React.Component<SelectProps, any> {
ref={this.inputRef} ref={this.inputRef}
value={selectedOptions} value={selectedOptions}
options={options} options={options}
onNewOptionClick={this.handleNewOptionClick}
loadOptions={ loadOptions={
isEffectiveApi(autoComplete) ? this.loadRemote : undefined isEffectiveApi(autoComplete) ? this.loadRemote : undefined
} }
creatable={creatable} creatable={creatable}
searchable={autoComplete || creatable ? true : searchable} searchable={searchable || !!autoComplete}
onChange={this.changeValue} onChange={this.changeValue}
loading={loading} loading={loading}
noResultsText={noResultsText} noResultsText={noResultsText}

View File

@ -3,12 +3,13 @@ import {OptionsControl, OptionsControlProps, highlight} from './Options';
import cx from 'classnames'; import cx from 'classnames';
import {Action} from '../../types'; import {Action} from '../../types';
import Downshift, {StateChangeOptions} from 'downshift'; import Downshift, {StateChangeOptions} from 'downshift';
// @ts-ignore
import matchSorter from 'match-sorter'; import matchSorter from 'match-sorter';
import debouce = require('lodash/debounce'); import debouce = require('lodash/debounce');
import {filter} from '../../utils/tpl'; import {filter} from '../../utils/tpl';
import find = require('lodash/find'); import find = require('lodash/find');
import {Icon} from '../../components/icons'; import {Icon} from '../../components/icons';
import {autobind, createObject} from '../../utils/helper'; import {autobind, createObject, setVariable} from '../../utils/helper';
import {isEffectiveApi} from '../../utils/api'; import {isEffectiveApi} from '../../utils/api';
// declare function matchSorter(items:Array<any>, input:any, options:any): Array<any>; // declare function matchSorter(items:Array<any>, input:any, options:any): Array<any>;
@ -88,7 +89,7 @@ export default class TextControl extends React.PureComponent<
} }
componentDidMount() { componentDidMount() {
const {formItem, autoComplete, data, addHook, formInited} = this.props; const {formItem, autoComplete, addHook, formInited, data} = this.props;
if (isEffectiveApi(autoComplete, data) && formItem) { if (isEffectiveApi(autoComplete, data) && formItem) {
if (formInited) { if (formInited) {
@ -99,16 +100,18 @@ export default class TextControl extends React.PureComponent<
}) })
); );
} else { } else {
this.unHook = addHook( this.unHook = addHook(async (data: any) => {
() => await formItem.loadOptions(
formItem.loadOptions( autoComplete,
autoComplete, createObject(data, {
createObject(data, { term: ''
term: '' })
}) );
),
'init' if (formItem.value) {
); setVariable(data, name!, formItem.value);
}
}, 'init');
} }
} }
} }
@ -447,7 +450,7 @@ export default class TextControl extends React.PureComponent<
filtedOptions.push({ filtedOptions.push({
[labelField || 'label']: this.state.inputValue, [labelField || 'label']: this.state.inputValue,
[valueField || 'value']: this.state.inputValue, [valueField || 'value']: this.state.inputValue,
isNew: true 'isNew': true
}); });
} }

View File

@ -2,11 +2,6 @@ import React from 'react';
import cx from 'classnames'; import cx from 'classnames';
import TreeSelector from '../../components/Tree'; import TreeSelector from '../../components/Tree';
import {OptionsControl, OptionsControlProps} from './Options'; import {OptionsControl, OptionsControlProps} from './Options';
import {autobind, createObject} from '../../utils/helper';
import {Action, Schema, PlainObject, Api, Payload} from '../../types';
import {isEffectiveApi} from '../../utils/api';
import {filter} from '../../utils/tpl';
import {Option} from '../../components/Checkboxes';
import {Spinner} from '../../components'; import {Spinner} from '../../components';
export interface TreeProps extends OptionsControlProps { export interface TreeProps extends OptionsControlProps {
@ -18,172 +13,25 @@ export interface TreeProps extends OptionsControlProps {
cascade?: boolean; // 父子之间是否完全独立。 cascade?: boolean; // 父子之间是否完全独立。
withChildren?: boolean; // 选父级的时候是否把子节点的值也包含在内。 withChildren?: boolean; // 选父级的时候是否把子节点的值也包含在内。
onlyChildren?: boolean; // 选父级的时候,是否只把子节点的值包含在内 onlyChildren?: boolean; // 选父级的时候,是否只把子节点的值包含在内
addApi?: Api; addControls?: Array<any>;
addMode?: 'dialog' | 'normal'; updateControls?: Array<any>;
addDialog?: Schema; rootCreatable?: boolean;
editApi?: Api;
editMode?: 'dialog' | 'normal';
editDialog?: Schema;
deleteApi?: Api;
deleteConfirmText?: string;
} }
export interface TreeState { export default class TreeControl extends React.Component<TreeProps> {
isAddModalOpened: boolean;
isEditModalOpened: boolean;
parent: Option | null;
prev: Option | null;
data: any;
}
export default class TreeControl extends React.Component<TreeProps, TreeState> {
static defaultProps: Partial<TreeProps> = { static defaultProps: Partial<TreeProps> = {
placeholder: '选项加载中...', placeholder: '选项加载中...',
multiple: false, multiple: false,
hideRoot: false,
rootLabel: '顶级', rootLabel: '顶级',
rootValue: '', rootValue: '',
showIcon: true showIcon: true
}; };
state: TreeState = {
isAddModalOpened: false,
isEditModalOpened: false,
parent: null,
prev: null,
data: null
};
reload() { reload() {
const reload = this.props.reloadOptions; const reload = this.props.reloadOptions;
reload && reload(); reload && reload();
} }
@autobind
handleAdd(values: PlainObject) {
this.saveRemote(values, 'add');
}
@autobind
handleAddModalConfirm(
values: Array<any>,
action: Action,
ctx: any,
components: Array<any>
) {
this.saveRemote(
{
...values,
parent: this.state.parent
},
'add'
);
this.closeAddDialog();
}
@autobind
handleEdit(values: PlainObject) {
this.saveRemote(values, 'edit');
}
@autobind
handleEditModalConfirm(
values: Array<any>,
action: Action,
ctx: any,
components: Array<any>
) {
this.saveRemote(
{
...values,
prev: this.state.prev
},
'edit'
);
this.closeEditDialog();
}
@autobind
async saveRemote(item: any, type: 'add' | 'edit') {
const {addApi, editApi, data, env} = this.props;
let remote: Payload | null = null;
if (type == 'add' && isEffectiveApi(addApi, createObject(data, item))) {
remote = await env.fetcher(addApi, createObject(data, item));
} else if (
type == 'edit' &&
isEffectiveApi(editApi, createObject(data, item))
) {
remote = await env.fetcher(editApi, createObject(data, item));
}
if (remote && !remote.ok) {
env.notify('error', remote.msg || '保存失败');
return;
}
this.reload();
}
@autobind
async handleRemove(item: any) {
const {deleteConfirmText, deleteApi, data, env} = this.props;
const ctx = createObject(data, item);
if (isEffectiveApi(deleteApi, ctx)) {
const confirmed = await env.confirm(
deleteConfirmText ? filter(deleteConfirmText, ctx) : '确认要删除?'
);
if (!confirmed) {
return;
}
const result = await env.fetcher(deleteApi, ctx);
if (!result.ok) {
env.notify('error', result.msg || '删除失败');
return;
}
this.reload();
}
}
@autobind
openAddDialog(parent: Option | null) {
const {data} = this.props;
this.setState({
isAddModalOpened: true,
data: createObject(data, parent ? parent : {}),
parent
});
}
@autobind
closeAddDialog() {
this.setState({
isAddModalOpened: false,
parent: null
});
}
@autobind
openEditDialog(prev: Option) {
const {data} = this.props;
this.setState({
isEditModalOpened: true,
data: createObject(data, prev),
prev
});
}
@autobind
closeEditDialog() {
this.setState({
isEditModalOpened: false,
prev: null
});
}
render() { render() {
const { const {
className, className,
@ -196,7 +44,6 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
delimiter, delimiter,
placeholder, placeholder,
options, options,
inline,
multiple, multiple,
valueField, valueField,
initiallyOpen, initiallyOpen,
@ -210,18 +57,17 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
rootValue, rootValue,
showIcon, showIcon,
showRadio, showRadio,
render, onAdd,
addMode, creatable,
addApi, addControls,
addDialog, onEdit,
editMode, editable,
editApi, editControls,
editDialog, removable,
deleteApi onDelete,
rootCreatable
} = this.props; } = this.props;
const {data} = this.state;
return ( return (
<div className={cx(`${ns}TreeControl`, className)}> <div className={cx(`${ns}TreeControl`, className)}>
<Spinner size="sm" key="info" show={loading} /> <Spinner size="sm" key="info" show={loading} />
@ -235,7 +81,7 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
extractValue={extractValue} extractValue={extractValue}
delimiter={delimiter} delimiter={delimiter}
placeholder={placeholder} placeholder={placeholder}
data={options} options={options}
multiple={multiple} multiple={multiple}
initiallyOpen={initiallyOpen} initiallyOpen={initiallyOpen}
unfoldedLevel={unfoldedLevel} unfoldedLevel={unfoldedLevel}
@ -249,52 +95,18 @@ export default class TreeControl extends React.Component<TreeProps, TreeState> {
cascade={cascade} cascade={cascade}
foldedField="collapsed" foldedField="collapsed"
value={value || ''} value={value || ''}
nameField="label" labelField="label"
selfDisabledAffectChildren={false} selfDisabledAffectChildren={false}
addMode={addMode} onAdd={onAdd}
addable={isEffectiveApi(addApi)} creatable={creatable}
onAdd={this.handleAdd} rootCreatable={rootCreatable}
openAddDialog={this.openAddDialog} onEdit={onEdit}
editMode={editMode} editable={editable}
editable={isEffectiveApi(editApi)} removable={removable}
onEdit={this.handleEdit} onDelete={onDelete}
openEditDialog={this.openEditDialog} bultinCUD={!addControls && !editControls}
onRemove={this.handleRemove}
removable={isEffectiveApi(deleteApi)}
/> />
)} )}
{addMode &&
render(
'modal',
{
type: 'dialog',
...addDialog
},
{
key: 'addModal',
data: data,
onConfirm: this.handleAddModalConfirm,
onClose: this.closeAddDialog,
show: this.state.isAddModalOpened
}
)}
{editMode &&
render(
'modal',
{
type: 'dialog',
...editDialog
},
{
key: 'editModal',
data: data,
onConfirm: this.handleEditModalConfirm,
onClose: this.closeEditDialog,
show: this.state.isEditModalOpened
}
)}
</div> </div>
); );
} }

View File

@ -6,6 +6,7 @@ import PopOver from '../../components/PopOver';
import {OptionsControl, OptionsControlProps, Option} from './Options'; import {OptionsControl, OptionsControlProps, Option} from './Options';
import {Icon} from '../../components/icons'; import {Icon} from '../../components/icons';
import TreeSelector from '../../components/Tree'; import TreeSelector from '../../components/Tree';
// @ts-ignore
import matchSorter from 'match-sorter'; import matchSorter from 'match-sorter';
import debouce = require('lodash/debounce'); import debouce = require('lodash/debounce');
import find = require('lodash/find'); import find = require('lodash/find');
@ -395,7 +396,6 @@ export default class TreeSelectControl extends React.Component<
return ( return (
<Overlay <Overlay
container={popOverContainer || (() => this.container.current)} container={popOverContainer || (() => this.container.current)}
placement="left-bottom-left-top right-bottom-right-top"
target={() => this.target.current} target={() => this.target.current}
show show
> >
@ -420,7 +420,7 @@ export default class TreeSelectControl extends React.Component<
extractValue={extractValue} extractValue={extractValue}
delimiter={delimiter} delimiter={delimiter}
placeholder={optionsPlaceholder} placeholder={optionsPlaceholder}
data={filtedOptions} options={filtedOptions}
highlightTxt={this.state.inputValue} highlightTxt={this.state.inputValue}
multiple={multiple} multiple={multiple}
initiallyOpen={initiallyOpen} initiallyOpen={initiallyOpen}
@ -434,7 +434,7 @@ export default class TreeSelectControl extends React.Component<
foldedField="collapsed" foldedField="collapsed"
hideRoot hideRoot
value={value || ''} value={value || ''}
nameField="label" labelField="label"
maxLength={maxLength} maxLength={maxLength}
minLength={minLength} minLength={minLength}
/> />

View File

@ -13,7 +13,9 @@ import {
until, until,
noop, noop,
isObject, isObject,
isVisible isVisible,
createObject,
extendObject
} from '../../utils/helper'; } from '../../utils/helper';
import debouce = require('lodash/debounce'); import debouce = require('lodash/debounce');
import flatten = require('lodash/flatten'); import flatten = require('lodash/flatten');
@ -314,12 +316,16 @@ export default class Form extends React.Component<FormProps, object> {
async onInit() { async onInit() {
const {onInit, store, submitOnInit} = this.props; const {onInit, store, submitOnInit} = this.props;
const data = store.data; // 先拿出来数据,主要担心 form 被什么东西篡改了,然后又应用出去了
// 之前遇到过问题,所以拿出来了。但是 options loadOptions 默认值失效了。
// 所以目前需要两个都要设置一下,再 init Hook 里面。
const data = {...store.data};
store.setInited(true); store.setInited(true);
const hooks: Array<(data: any) => Promise<any>> = this.hooks['init'] || []; const hooks: Array<(data: any) => Promise<any>> = this.hooks['init'] || [];
await Promise.all(hooks.map(hook => hook(data))); await Promise.all(hooks.map(hook => hook(data)));
onInit && onInit(data); onInit && onInit(extendObject(store.data, data));
submitOnInit && submitOnInit &&
this.handleAction( this.handleAction(

View File

@ -421,7 +421,6 @@ export const HocQuickEdit = (config: Partial<QuickEditConfig> = {}) => (
return ( return (
<Overlay <Overlay
container={popOverContainer} container={popOverContainer}
placement="left-top right-top left-bottom right-bottom"
target={() => this.target} target={() => this.target}
onHide={this.closeQuickEdit} onHide={this.closeQuickEdit}
show show

View File

@ -5,17 +5,12 @@ import {Api, Payload, fetchOptions} from '../types';
import {ComboStore, IComboStore, IUniqueGroup} from './combo'; import {ComboStore, IComboStore, IUniqueGroup} from './combo';
import {evalExpression} from '../utils/tpl'; import {evalExpression} from '../utils/tpl';
import findIndex = require('lodash/findIndex'); import findIndex = require('lodash/findIndex');
import { import {isArrayChilrenModified, isObject, createObject} from '../utils/helper';
isArrayChilrenModified,
hasOwnProperty,
isObject,
createObject
} from '../utils/helper';
import {flattenTree} from '../utils/helper'; import {flattenTree} from '../utils/helper';
import {IRendererStore} from '.'; import {IRendererStore} from '.';
import {normalizeOptions} from '../components/Select'; import {normalizeOptions} from '../components/Select';
import find = require('lodash/find'); import find = require('lodash/find');
import {iRendererStore} from './iRenderer'; import {SimpleMap} from '../utils/SimpleMap';
interface IOption { interface IOption {
value?: string | number | null; value?: string | number | null;
@ -34,6 +29,7 @@ const ErrorDetail = types.model('ErrorDetail', {
export const FormItemStore = types export const FormItemStore = types
.model('FormItemStore', { .model('FormItemStore', {
identifier: types.identifier, identifier: types.identifier,
isFocused: false,
type: '', type: '',
unique: false, unique: false,
loading: false, loading: false,
@ -81,41 +77,6 @@ export const FormItemStore = types
return self.errorData.map(item => item.msg); return self.errorData.map(item => item.msg);
} }
// function selectedOptions(options:Array<Option>=(self.options as any).toJS()) {
// return value2array(getValue(), {
// multiple: self.multiple,
// delimiter: self.delimiter,
// valueField: self.valueField,
// options: options
// })
// }
// function filteredOptions(data:object):Array<IOption> {
// let options:Array<IOption> = self.options;
// options = options.filter(item => {
// let filtered = getExprProperties(item, data);
// return filtered.visible !== false && !filtered.hidden;
// });
// let parentStore = getForm().parentStore;
// if (parentStore && parentStore.storeType === ComboStore.name) {
// let combo = parentStore as IComboStore;
// let group = combo.uniques.get(self.name) as IUniqueGroup;
// let selectedOptions:Array<any> = [];
// group && group.items.forEach(item => {
// if (self !== item) {
// selectedOptions.push(...item.selectedOptions().map(item => item.value))
// }
// });
// if (selectedOptions.length) {
// options = options.filter(option => !~selectedOptions.indexOf(option.value))
// }
// }
// return options;
// }
return { return {
get form(): any { get form(): any {
return getForm(); return getForm();
@ -144,9 +105,6 @@ export const FormItemStore = types
return getLastOptionValue(); return getLastOptionValue();
}, },
// selectedOptions,
// filteredOptions,
getSelectedOptions(value: any = getValue()) { getSelectedOptions(value: any = getValue()) {
if (value === getValue()) { if (value === getValue()) {
return self.selectedOptions; return self.selectedOptions;
@ -212,6 +170,9 @@ export const FormItemStore = types
}) })
.actions(self => { .actions(self => {
const form = self.form as IFormStore;
const dialogCallbacks = new SimpleMap<(result?: any) => void>();
function config({ function config({
required, required,
unique, unique,
@ -241,8 +202,6 @@ export const FormItemStore = types
type?: string; type?: string;
id?: string; id?: string;
}) { }) {
const form = self.form as IFormStore;
if (typeof rules === 'string') { if (typeof rules === 'string') {
rules = str2rules(rules); rules = str2rules(rules);
} }
@ -278,6 +237,14 @@ export const FormItemStore = types
} }
} }
function focus() {
self.isFocused = true;
}
function blur() {
self.isFocused = false;
}
function changeValue(value: any, isPrintine: boolean = false) { function changeValue(value: any, isPrintine: boolean = false) {
if (typeof value === 'undefined' || value === '__undefined') { if (typeof value === 'undefined' || value === '__undefined') {
self.form.deleteValueByName(self.name); self.form.deleteValueByName(self.name);
@ -366,7 +333,7 @@ export const FormItemStore = types
options?: fetchOptions, options?: fetchOptions,
clearValue?: boolean, clearValue?: boolean,
onChange?: (value: any) => void onChange?: (value: any) => void
) => Promise<any> = flow(function* getInitData( ) => Promise<Payload | null> = flow(function* getInitData(
api: string, api: string,
data: object, data: object,
options?: fetchOptions, options?: fetchOptions,
@ -442,8 +409,9 @@ export const FormItemStore = types
console.error(e.stack); console.error(e.stack);
getRoot(self) && getRoot(self) &&
(getRoot(self) as IRendererStore).notify('error', e.message); (getRoot(self) as IRendererStore).notify('error', e.message);
return null;
} }
}); } as any);
function syncOptions(originOptions?: Array<any>) { function syncOptions(originOptions?: Array<any>) {
if (!self.options.length && typeof self.value === 'undefined') { if (!self.options.length && typeof self.value === 'undefined') {
@ -528,7 +496,7 @@ export const FormItemStore = types
unMatched = { unMatched = {
[self.valueField || 'value']: item, [self.valueField || 'value']: item,
[self.labelField || 'label']: item, [self.labelField || 'label']: item,
__unmatched: true '__unmatched': true
}; };
const orgin: any = const orgin: any =
@ -595,27 +563,30 @@ export const FormItemStore = types
clearError(); clearError();
} }
function openDialog(schema: any, ctx: any, additonal?: object) { function openDialog(
let proto = ctx.__super ? ctx.__super : self.form.data; schema: any,
data: any = form.data,
if (additonal) { callback?: (ret?: any) => void
proto = createObject(proto, additonal); ) {
}
const data = createObject(proto, {
...ctx
});
self.dialogSchema = schema; self.dialogSchema = schema;
self.dialogData = data; self.dialogData = data;
self.dialogOpen = true; self.dialogOpen = true;
callback && dialogCallbacks.set(self.dialogData, callback);
} }
function closeDialog() { function closeDialog(result?: any) {
const callback = dialogCallbacks.get(self.dialogData);
self.dialogOpen = false; self.dialogOpen = false;
if (callback) {
dialogCallbacks.delete(self.dialogData);
setTimeout(() => callback(result), 200);
}
} }
return { return {
focus,
blur,
config, config,
changeValue, changeValue,
validate, validate,

View File

@ -2,6 +2,7 @@ import {types, getRoot, Instance} from 'mobx-state-tree';
import {extendObject, createObject} from '../utils/helper'; import {extendObject, createObject} from '../utils/helper';
import {IRendererStore} from './index'; import {IRendererStore} from './index';
import {dataMapping} from '../utils/tpl-builtin'; import {dataMapping} from '../utils/tpl-builtin';
import {SimpleMap} from '../utils/SimpleMap';
export const iRendererStore = types export const iRendererStore = types
.model('iRendererStore', { .model('iRendererStore', {
@ -32,7 +33,7 @@ export const iRendererStore = types
}; };
}) })
.actions(self => { .actions(self => {
const dialogCallbacks = new Map(); const dialogCallbacks = new SimpleMap<(result?: any) => void>();
return { return {
initData(data: object = {}) { initData(data: object = {}) {
@ -97,10 +98,7 @@ export const iRendererStore = types
self.dialogData = data; self.dialogData = data;
} }
self.dialogOpen = true; self.dialogOpen = true;
callback && dialogCallbacks.set(self.dialogData, callback);
if (callback) {
dialogCallbacks.set(self.dialogData, callback);
}
}, },
closeDialog(result?: any) { closeDialog(result?: any) {

30
src/utils/SimpleMap.ts Normal file
View File

@ -0,0 +1,30 @@
import find = require('lodash/find');
import findIndex = require('lodash/findIndex');
export class SimpleMap<V = any, K = any> {
private readonly list: Array<{
key: K;
value: V;
}> = [];
set(key: K, value: V) {
this.list.push({
key,
value
});
}
get(key: K) {
const resolved = find(this.list, item => item.key === key);
return resolved ? resolved.value : null;
}
delete(key: K) {
const idx = findIndex(this.list, item => item.key === key);
~idx && this.list.splice(idx, 1);
}
dispose() {
this.list.splice(0, this.list.length);
}
}

View File

@ -20,13 +20,13 @@ interface ApiCacheConfig extends ApiObject {
const apiCaches: Array<ApiCacheConfig> = []; const apiCaches: Array<ApiCacheConfig> = [];
export function normalizeApi(api: Api): ApiObject { export function normalizeApi(api: Api, defaultMethod?: string): ApiObject {
if (typeof api === 'string') { if (typeof api === 'string') {
let method = rSchema.test(api) ? RegExp.$1 : ''; let method = rSchema.test(api) ? RegExp.$1 : '';
method && (api = api.replace(method + ':', '')); method && (api = api.replace(method + ':', ''));
api = { api = {
method: method as any, method: (method || defaultMethod) as any,
url: api url: api
}; };
} else { } else {
@ -46,7 +46,7 @@ export function buildApi(
[propName: string]: any; [propName: string]: any;
} = {} } = {}
): ApiObject { ): ApiObject {
api = normalizeApi(api); api = normalizeApi(api, options.method);
const {autoAppend, ignoreData, ...rest} = options; const {autoAppend, ignoreData, ...rest} = options;
api.config = { api.config = {

View File

@ -132,6 +132,12 @@ export function calculatePosition(
: getPosition(target, container); : getPosition(target, container);
const {height: overlayHeight, width: overlayWidth} = getOffset(overlayNode); const {height: overlayHeight, width: overlayWidth} = getOffset(overlayNode);
// auto 尝试四个方向对齐。
placement =
placement === 'auto'
? 'left-bottom-left-top right-bottom-right-top left-top-left-bottom right-top-right-bottom left-bottom-left-top'
: placement;
let positionLeft = 0, let positionLeft = 0,
positionTop = 0, positionTop = 0,
arrowOffsetLeft: any = '', arrowOffsetLeft: any = '',

View File

@ -373,6 +373,19 @@ export function isDisabled(
); );
} }
export function hasAbility(
schema: any,
ability: string,
data?: object,
defaultValue: boolean = true
): boolean {
return schema.hasOwnProperty(ability)
? schema[ability]
: schema.hasOwnProperty(`${ability}On`)
? evalExpression(schema[`${ability}On`], data || schema)
: defaultValue;
}
export function makeHorizontalDeeper( export function makeHorizontalDeeper(
horizontal: { horizontal: {
left: string; left: string;
@ -691,6 +704,11 @@ export function mapTree<T extends TreeItem>(
}); });
} }
/**
*
* @param tree
* @param iterator
*/
export function eachTree<T extends TreeItem>( export function eachTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => any, iterator: (item: T, key: number, level: number) => any,
@ -705,15 +723,19 @@ export function eachTree<T extends TreeItem>(
}); });
} }
/**
*
* @param tree
* @param iterator
*/
export function findTree<T extends TreeItem>( export function findTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => any, iterator: (item: T, key: number, level: number, paths: Array<T>) => any
level: number = 1
): T | null { ): T | null {
let result: T | null = null; let result: T | null = null;
everyTree(tree, (item, key, level) => { everyTree(tree, (item, key, level, paths) => {
if (iterator(item, key, level)) { if (iterator(item, key, level, paths)) {
result = item; result = item;
return false; return false;
} }
@ -723,6 +745,64 @@ export function findTree<T extends TreeItem>(
return result; return result;
} }
/**
* ,
* @param tree
* @param iterator
*/
export function findTreeIndex<T extends TreeItem>(
tree: Array<T>,
iterator: (item: T, key: number, level: number, paths: Array<T>) => any
): Array<number> | undefined {
let idx: Array<number> = [];
findTree(tree, (item, index, level, paths) => {
if (iterator(item, index, level, paths)) {
idx = [index];
paths = paths.concat();
paths.unshift({
children: tree
} as any);
for (let i = paths.length - 1; i > 0; i--) {
const prev = paths[i - 1];
const current = paths[i];
idx.unshift(prev.children!.indexOf(current));
}
return true;
}
return false;
});
return idx.length ? idx : undefined;
}
export function getTree<T extends TreeItem>(
tree: Array<T>,
idx: Array<number> | number
): T | undefined | null {
const indexes = Array.isArray(idx) ? idx : [idx];
const lastIndex = indexes.pop()!;
let list: Array<T> | null = tree;
for (let i = 0, len = indexes.length; i < len; i++) {
const index = indexes[i];
if (!list![index]) {
list = null;
break;
}
list = list![index].children as any;
}
return list ? list[lastIndex] : undefined;
}
/**
*
*
* @param tree
* @param iterator
*/
export function filterTree<T extends TreeItem>( export function filterTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean, iterator: (item: T, key: number, level: number) => boolean,
@ -741,44 +821,111 @@ export function filterTree<T extends TreeItem>(
}); });
} }
/**
*
* @param tree
* @param iterator
*/
export function everyTree<T extends TreeItem>( export function everyTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean, iterator: (item: T, key: number, level: number, paths: Array<T>) => boolean,
level: number = 1 level: number = 1,
paths: Array<T> = []
): boolean { ): boolean {
return tree.every((item, index) => { return tree.every((item, index) => {
const value: any = iterator(item, index, level); const value: any = iterator(item, index, level, paths);
if (value && item.children && item.children.splice) { if (value && item.children && item.children.splice) {
return everyTree(item.children, iterator, level + 1); return everyTree(item.children, iterator, level + 1, paths.concat(item));
} }
return value; return value;
}); });
} }
/**
*
* @param tree
* @param iterator
*/
export function someTree<T extends TreeItem>( export function someTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean, iterator: (item: T, key: number, level: number, paths: Array<T>) => boolean
level: number = 1
): boolean { ): boolean {
return tree.some((item, index) => { return !everyTree(tree, iterator);
const value: any = iterator(item, index, level);
if (!value && item.children && item.children.splice) {
return someTree(item.children, iterator, level + 1);
}
return value;
});
} }
export function flattenTree<T extends TreeItem>(tree: Array<T>): Array<T> { /**
let flattened: Array<T> = []; *
eachTree(tree, item => flattened.push(item)); *
*
*
* flattenTree([
* {
* id: 1,
* children: [
* { id: 2 },
* { id: 3 },
* ]
* }
* ], item => item.id); // 输出位 [1, 2, 3]
*
* @param tree
* @param mapper
*/
export function flattenTree<T extends TreeItem>(tree: Array<T>): Array<T>;
export function flattenTree<T extends TreeItem, U>(
tree: Array<T>,
mapper: (value: T, index: number) => U
): Array<U>;
export function flattenTree<T extends TreeItem, U>(
tree: Array<T>,
mapper?: (value: T, index: number) => U
): Array<U> {
let flattened: Array<any> = [];
eachTree(tree, (item, index) =>
flattened.push(mapper ? mapper(item, index) : item)
);
return flattened; return flattened;
} }
/**
* imutable,
* splice
*
*
* findTreeIndex
*
* @param tree
* @param idx
* @param deleteCount
* @param ...items
*/
export function spliceTree<T extends TreeItem>(
tree: Array<T>,
idx: Array<number> | number,
deleteCount: number = 0,
...items: Array<T>
): Array<T> {
const list = tree.concat();
if (typeof idx === 'number') {
list.splice(idx, deleteCount, ...items);
} else if (Array.isArray(idx) && idx.length) {
const lastIdx = idx.pop()!;
let host = idx.reduce((list: Array<T>, idx) => {
const child = {
...list[idx],
children: list[idx].children ? list[idx].children!.concat() : []
};
list[idx] = child;
return child.children;
}, list);
host.splice(lastIdx, deleteCount, ...items);
}
return list;
}
export function ucFirst(str?: string) { export function ucFirst(str?: string) {
return str ? str.substring(0, 1).toUpperCase() + str.substring(1) : ''; return str ? str.substring(0, 1).toUpperCase() + str.substring(1) : '';
} }