select search 框放到 popOver 中并支持新增\编辑\删除

This commit is contained in:
liaoxuezhi 2019-11-08 10:26:21 +08:00
parent e2d93eae04
commit 9d4b983d9e
16 changed files with 615 additions and 352 deletions

View File

@ -8,10 +8,7 @@
background: $Form-select-bg;
border-radius: $Form-select-borderRadius;
height: $Form-selectOption-height;
$paddingY: (
$Form-selectOption-height - $Form-input-lineHeight *
$Form-input-fontSize - $Form-select-borderWidth * 2
)/2;
$paddingY: ($Form-selectOption-height - $Form-input-lineHeight * $Form-input-fontSize - $Form-select-borderWidth * 2)/2;
padding: $paddingY 0 $paddingY $Form-select-paddingX;
cursor: pointer;
color: $Form-select-color;
@ -46,17 +43,7 @@
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 {
line-height: $Form-input-lineHeight * $Form-input-fontSize;
@ -64,6 +51,7 @@
}
&--searchable {
.#{$ns}Select-placeholder,
.#{$ns}Select-value {
position: absolute;
@ -79,22 +67,21 @@
.#{$ns}Select-valueWrap {
margin-bottom: -$gap-xs;
> input {
>input {
display: inline-block;
width: px2rem(100px);
margin-bottom: $gap-xs;
}
}
.#{$ns}Select-values + .#{$ns}Select-input {
.#{$ns}Select-values+.#{$ns}Select-input {
transform: translateY(0);
}
.#{$ns}Select-value {
position: static;
user-select: none;
line-height: $Form-input-lineHeight * $Form-input-fontSize -
px2rem(2px);
line-height: $Form-input-lineHeight * $Form-input-fontSize - px2rem(2px);
display: inline-block;
vertical-align: middle;
font-size: $Form-selectValue-fontSize;
@ -145,19 +132,18 @@
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;
}
// &-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 {
max-height: px2rem(300px);
@ -165,24 +151,37 @@
user-select: none;
}
&-checkAll {
padding: (
$Form-select-menu-height - $Form-input-lineHeight *
$Form-input-fontSize - px2rem(2px)
)/2 $Form-select-paddingX;
border-bottom: px2rem(1px) solid $Form-select-checkall-bottomBorder;
min-width: px2rem(100px);
&-input {
cursor: pointer;
outline: none;
border: none;
margin: 0 $Form-select-paddingX;
padding: ($Form-input-height - $Form-input-lineHeight * $Form-input-fontSize)/2 0;
line-height: $Form-input-lineHeight;
border-bottom: 1px solid $borderColor;
display: flex;
align-items: center;
label {
display: block;
>svg {
fill: #999;
width: px2rem(14px);
height: px2rem(14px);
margin-right: px2rem(5px);
top: 0;
}
>input {
outline: none;
border: none;
flex-grow: 1;
background: transparent;
font-size: px2rem(12px);
}
}
&-option {
padding: (
$Form-select-menu-height - $Form-input-lineHeight *
$Form-input-fontSize - px2rem(2px)
)/2 $Form-select-paddingX;
cursor: pointer;
padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
&.is-active {
color: $Form-select-menu-onActive-color;
@ -202,12 +201,46 @@
&--placeholder {
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;
user-select: none;
margin: 5px 0 0;
padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
}
&-option-hl {
color: $red;
}
&-addBtn {
display: block;
cursor: pointer;
padding: ($Form-select-menu-height - $Form-input-lineHeight * $Form-input-fontSize)/2 $Form-select-paddingX;
>svg {
width: px2rem(14px);
height: px2rem(14px);
margin-right: $Checkbox-gap;
}
}
&.is-focused,
&.is-opened {
border-color: $Form-input-onFocused-borderColor;
@ -243,28 +276,33 @@
}
.#{$ns}Select-popover {
margin-top: -$Form-select-borderWidth;
margin-top: px2rem(2px);
background: $Form-select-menu-bg;
color: $Form-select-menu-color;
border: $Form-select-outer-borderWidth solid
$Form-input-onFocused-borderColor;
border: $Form-select-outer-borderWidth solid $Form-input-onFocused-borderColor;
box-shadow: $Form-select-outer-boxShadow;
border-top-left-radius: 0;
border-top-right-radius: 0;
min-width: px2rem(100px);
z-index: 2;
&.#{$ns}PopOver--leftTopLeftBottom {
margin-top: px2rem(-2px);
}
}
.#{$ns}SelectControl:not(.is-inline) > .#{$ns}Select {
.#{$ns}SelectControl:not(.is-inline)>.#{$ns}Select {
display: flex;
}
// 需要能撑开
@include media-breakpoint-up(sm) {
.#{$ns}Form-control--sizeXs > .#{$ns}Select,
.#{$ns}Form-control--sizeSm > .#{$ns}Select,
.#{$ns}Form-control--sizeMd > .#{$ns}Select,
.#{$ns}Form-control--sizeLg > .#{$ns}Select {
.#{$ns}Form-control--sizeXs>.#{$ns}Select,
.#{$ns}Form-control--sizeSm>.#{$ns}Select,
.#{$ns}Form-control--sizeMd>.#{$ns}Select,
.#{$ns}Form-control--sizeLg>.#{$ns}Select {
min-width: 100%;
display: inline-flex !important;
}
}
}

View File

@ -102,6 +102,7 @@ $Form-select-outer-boxShadow: px2rem(2px) px2rem(4px) px2rem(8px) rgba(0, 0, 0,
$Form-select-menu-color: #333;
$Form-select-menu-onHover-color: #000;
$Form-select-menu-onHover-bg: #eaf6fe;
$Form-select-menu-height: px2rem(24px);
$InputGroup-select-borderWidth: px2rem(1px);
$InputGroup-select-bg: #f6f7fb;

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,6 @@
import {Api, Schema} from '../../types';
import {
buildApi,
isEffectiveApi,
isValidApi,
isApiOutdated
} from '../../utils/api';
import {anyChanged, autobind} from '../../utils/helper';
import {isEffectiveApi, isApiOutdated} from '../../utils/api';
import {anyChanged, autobind, createObject} from '../../utils/helper';
import {reaction} from 'mobx';
import {FormControlProps, registerFormItem, FormItemBasicConfig} from './Item';
import {IFormItemStore} from '../../store/formItem';
@ -13,8 +8,9 @@ export type OptionsControlComponent = React.ComponentType<FormControlProps>;
import React from 'react';
import {resolveVariableAndFilter} from '../../utils/tpl-builtin';
import {evalExpression} from '../../utils/tpl';
import {Option, OptionProps, normalizeOptions} from '../../components/Select';
import {filter} from '../../utils/tpl';
import findIndex from 'lodash/findIndex';
export {Option};
@ -26,6 +22,7 @@ export interface OptionsConfig extends OptionsBasicConfig {
component: React.ComponentType<OptionsControlProps>;
}
// 下发给注册进来的组件的属性。
export interface OptionsControlProps extends FormControlProps, OptionProps {
source?: Api;
name?: string;
@ -35,22 +32,24 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
setOptions: (value: Array<any>) => void;
setLoading: (value: boolean) => void;
reloadOptions: () => void;
addable?: boolean;
creatable?: boolean;
onAdd?: () => void;
addControls?: Array<any>;
editable?: boolean;
editControls?: Array<any>;
onEdit?: (value: Option) => void;
removable?: boolean;
onDelete?: (value: Option) => void;
}
// 自己接收的属性。
export interface OptionsProps extends FormControlProps, OptionProps {
sourcce?: Api;
source?: Api;
creatable?: boolean;
addApi?: Api;
addMode?: 'dialog' | 'normal';
addDialog?: Schema;
addControls?: Array<any>;
editApi?: Api;
editMode?: 'dialog' | 'normal';
editDialog?: Schema;
editControls?: Array<any>;
deleteApi?: Api;
deleteConfirmText?: string;
}
@ -58,7 +57,6 @@ export interface OptionsProps extends FormControlProps, OptionProps {
export function registerOptionsControl(config: OptionsConfig) {
const Control = config.component;
// @observer
class FormOptionsItem extends React.Component<OptionsProps, any> {
static displayName = `OptionsControl(${config.type})`;
static defaultProps = {
@ -70,6 +68,7 @@ export function registerOptionsControl(config: OptionsConfig) {
multiple: false,
placeholder: '请选择',
resetValue: '',
deleteConfirmText: '确定要删除?',
...Control.defaultProps
};
static propsList: any = (Control as any).propsList
@ -113,10 +112,10 @@ export function registerOptionsControl(config: OptionsConfig) {
let loadOptions: boolean = initFetch !== false;
if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(source) && formItem) {
if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(source as string) && formItem) {
formItem.setOptions(
normalizeOptions(
resolveVariableAndFilter(source, data, '| raw') || []
resolveVariableAndFilter(source as string, data, '| raw') || []
)
);
loadOptions = false;
@ -207,7 +206,7 @@ export function registerOptionsControl(config: OptionsConfig) {
) {
if (/^\$(?:([a-z0-9_.]+)|{.+})$/.test(props.source as string)) {
const prevOptions = resolveVariableAndFilter(
prevProps.source,
prevProps.source as string,
prevProps.data,
'| raw'
);
@ -439,8 +438,189 @@ export function registerOptionsControl(config: OptionsConfig) {
formItem && formItem.setLoading(value);
}
@autobind
async handleOptionAdd() {
let {
addControls,
disabled,
labelField,
onOpenDialog,
addApi,
source,
data,
valueField,
formItem: model,
createBtnLabel
} = this.props;
if (disabled || !model) {
return;
}
if (!Array.isArray(addControls) || !addControls.length) {
addControls = [
{
type: 'text',
name: labelField || 'label',
label: false,
placeholder: '请输入名称'
}
];
}
let result: any = await onOpenDialog(
{
type: 'dialog',
title: createBtnLabel || '新增选项',
body: {
type: 'form',
api: addApi,
controls: addControls
}
},
data
);
if (result) {
// 没走服务端的。
if (!result.__saved) {
result = {
...result,
[valueField || 'value']: result[labelField || 'label']
};
}
source
? this.reload()
: model.setOptions(model.options.concat({...result}));
}
}
@autobind
async handleOptionEdit(value: any) {
let {
editControls,
disabled,
labelField,
onOpenDialog,
editApi,
source,
data,
formItem: model,
valueField
} = this.props;
if (disabled || !model) {
return;
}
if (!Array.isArray(editControls) || !editControls.length) {
editControls = [
{
type: 'text',
name: labelField || 'label',
label: false,
placeholder: '请输入名称'
}
];
}
let result = await onOpenDialog(
{
type: 'dialog',
title: '编辑选项',
body: {
type: 'form',
api: editApi,
controls: editControls
}
},
createObject(data, value)
);
if (!result) {
return;
}
if (source) {
this.reload();
} else {
const options = model.options.concat();
const idx = findIndex(
options,
item => item[valueField || 'value'] == result[valueField || 'value']
);
if (~idx) {
options.splice(idx, 1, {
...options[idx],
...result
});
model.setOptions(options);
}
}
}
@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);
const confirmed = deleteConfirmText
? await env.confirm(filter(deleteConfirmText, ctx))
: true;
if (!confirmed) {
return;
}
try {
const result = await env.fetcher(deleteApi!, ctx);
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() {
const {value, formItem} = this.props;
const {
value,
formItem,
addApi,
editApi,
deleteApi,
creatable,
editable
} = this.props;
return (
<Control
@ -455,6 +635,12 @@ export function registerOptionsControl(config: OptionsConfig) {
setOptions={this.setOptions}
syncOptions={this.syncOptions}
reloadOptions={this.reload}
creatable={creatable || isEffectiveApi(addApi)}
editable={editable || isEffectiveApi(editApi)}
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
});
this.inputRef = this.inputRef.bind(this);
this.handleNewOptionClick = this.handleNewOptionClick.bind(this);
}
inputRef(ref: any) {
@ -161,16 +160,6 @@ export default class SelectControl extends React.Component<SelectProps, any> {
return combinedOptions;
}
handleNewOptionClick(option: any) {
const {setOptions, options} = this.props;
let mergedOptions: Array<any> = options.concat();
mergedOptions.push({
...option
});
setOptions(mergedOptions);
}
reload() {
const reload = this.props.reloadOptions;
reload && reload();
@ -211,7 +200,6 @@ export default class SelectControl extends React.Component<SelectProps, any> {
ref={this.inputRef}
value={selectedOptions}
options={options}
onNewOptionClick={this.handleNewOptionClick}
loadOptions={
isEffectiveApi(autoComplete) ? this.loadRemote : undefined
}

View File

@ -395,7 +395,6 @@ export default class TreeSelectControl extends React.Component<
return (
<Overlay
container={popOverContainer || (() => this.container.current)}
placement="left-bottom-left-top right-bottom-right-top"
target={() => this.target.current}
show
>

View File

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

View File

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

View File

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