添加 InputBox Transfer 组件

This commit is contained in:
2betop 2020-05-13 19:28:46 +08:00
parent 98077b9637
commit 4b2985404d
11 changed files with 619 additions and 45 deletions

View File

@ -355,6 +355,18 @@
}
.#{$ns}TableCheckboxes {
.#{$ns}Table-table > thead > tr > th,
.#{$ns}Table-table > tbody > tr > td {
font-size: $fontSizeSm;
padding-top: px2rem(5px);
padding-bottom: px2rem(6px);
vertical-align: middle;
}
.#{$ns}Table-table > thead > tr > th {
padding-top: px2rem(6px);
}
.#{$ns}Table-table > thead > tr > th:first-child,
.#{$ns}Table-table > tbody > tr > td:first-child {
padding-left: px2rem(10px);

View File

@ -17,6 +17,35 @@
}
}
@mixin input-input {
display: flex;
background-color: $Form-input-bg;
border: $Form-input-borderWidth solid $Form-input-borderColor;
border-radius: $Form-input-borderRadius;
// height: $Form-input-height;
line-height: $Form-input-lineHeight;
padding: $Form-input-paddingY $Form-input-paddingX;
font-size: $Form-input-fontSize;
input {
outline: none;
background: transparent;
border: none;
color: $Form-input-color;
width: 100%;
height: $Form-input-lineHeight * $Form-input-fontSize;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
}
> input {
flex-grow: 1;
}
}
@mixin input-text {
position: relative;
min-width: $Form-control-widthBase;
@ -26,32 +55,7 @@
}
&-input {
display: flex;
background-color: $Form-input-bg;
border: $Form-input-borderWidth solid $Form-input-borderColor;
border-radius: $Form-input-borderRadius;
// height: $Form-input-height;
line-height: $Form-input-lineHeight;
padding: $Form-input-paddingY $Form-input-paddingX;
font-size: $Form-input-fontSize;
input {
outline: none;
background: transparent;
border: none;
color: $Form-input-color;
width: 100%;
height: $Form-input-lineHeight * $Form-input-fontSize;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
}
> input {
flex-grow: 1;
}
@include input-input();
}
&.is-error > &-input {
@ -325,3 +329,45 @@
padding: 0 $gap-xs;
}
}
.#{$ns}InputBox {
@include input-input();
&.is-error {
border-color: $Form-input-onError-borderColor;
background-color: $Form-input-onError-bg;
}
&.is-focused {
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;
}
}
&.is-error.is-focused {
border-color: $Form-input-onError-borderColor;
}
&.is-disabled {
color: $text--muted-color;
background: $Form-input-onDisabled-bg;
border-color: $Form-input-onDisabled-borderColor;
}
&-clear {
@include input-clear();
}
> svg {
display: inline-block;
width: 14px;
color: $icon-color;
}
> a {
cursor: pointer;
}
}

View File

@ -0,0 +1,61 @@
.#{$ns}Transfer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
min-height: px2rem(300px);
&--inline {
display: inline-flex;
flex-wrap: nowrap;
}
&-title {
display: flex;
background: $Table-thead-bg;
height: px2rem(30px);
line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize;
padding: (px2rem(30px) - $Form-input-lineHeight * $Form-input-fontSize)/2
$gap-sm;
flex-direction: row;
&--light {
background: transparent;
}
> span {
flex-grow: 1;
}
}
&-select,
&-result {
min-width: px2rem(200px);
flex-basis: px2rem(200px);
flex-grow: 1;
border: $Form-input-borderWidth solid $Form-input-borderColor;
display: flex;
flex-direction: column;
}
&-checkboxes,
&-selections {
flex-grow: 1;
height: 0;
overflow: auto;
}
&-search {
padding: $gap-sm;
}
&-arrow {
width: px2rem(10px);
}
&-checkAll,
&-clearAll {
user-select: none;
cursor: pointer;
}
}

View File

@ -572,6 +572,7 @@ $Combo--horizontal-dragger-top: px2rem(5px);
@import '../components/form/qr-code';
@import '../components/form/tag';
@import '../components/form/rating';
@import '../components/form/transfer';
@import '../components/form/transfer-select';
@import '../components/form/nested-select';
@import '../components/form/icon-picker';

View File

@ -235,6 +235,7 @@ pre {
@import '../components/form/qr-code';
@import '../components/form/tag';
@import '../components/form/rating';
@import '../components/form/transfer';
@import '../components/form/transfer-select';
@import '../components/form/nested-select';
@import '../components/form/icon-picker';

View File

@ -100,6 +100,7 @@ $Form-input-borderColor: #cfdadd;
@import '../components/form/qr-code';
@import '../components/form/tag';
@import '../components/form/rating';
@import '../components/form/transfer';
@import '../components/form/transfer-select';
@import '../components/form/nested-select';
@import '../components/form/icon-picker';

View File

@ -11,7 +11,7 @@ import chunk from 'lodash/chunk';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {Option, value2array, Options} from './Select';
import find from 'lodash/find';
import { autobind, findTree } from '../utils/helper';
import {autobind, findTree} from '../utils/helper';
// import isPlainObject from 'lodash/isPlainObject';
export interface CheckboxesProps extends ThemeProps {
@ -29,7 +29,10 @@ export interface CheckboxesProps extends ThemeProps {
disabled?: boolean;
}
export class Checkboxes<T extends CheckboxesProps = CheckboxesProps, S = any> extends React.Component<T, S> {
export class Checkboxes<
T extends CheckboxesProps = CheckboxesProps,
S = any
> extends React.Component<T, S> {
static defaultProps = {
placeholder: '暂无选项',
itemRender: (option: Option) => <span>{option.label}</span>
@ -48,15 +51,15 @@ export class Checkboxes<T extends CheckboxesProps = CheckboxesProps, S = any> ex
value = [value];
}
return value
.map((value: any) => {
const option = findTree(options, option => option2value(option) === value);
return option;
})
.filter((item: any) => item);
return value.map((value: any) => {
const option = findTree(
options,
option => option2value(option) === value
);
return option || value;
});
}
@autobind
toggleOption(option: Option) {
const {value, onChange, option2value, options} = this.props;
@ -77,23 +80,23 @@ export class Checkboxes<T extends CheckboxesProps = CheckboxesProps, S = any> ex
? valueArray.map(item => option2value(item))
: valueArray;
onChange?.(newValue);
onChange && onChange(newValue);
}
@autobind
toggleAll() {
const {value, onChange, option2value, options} = this.props;
let valueArray:Array<Option> = [];
let valueArray: Array<Option> = [];
if (!Array.isArray(value) || !value.length) {
valueArray = options.filter(option => !option.disabled);
}
let newValue: string | Array<Option> = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange?.(newValue);
onChange && onChange(newValue);
}
render() {
@ -131,10 +134,14 @@ export class Checkboxes<T extends CheckboxesProps = CheckboxesProps, S = any> ex
}
return (
<div className={cx('Checkboxes', className, inline ? 'Checkboxes--inline' : '')}>
{body && body.length ? body : (
<div>{placeholder}</div>
<div
className={cx(
'Checkboxes',
className,
inline ? 'Checkboxes--inline' : ''
)}
>
{body && body.length ? body : <div>{placeholder}</div>}
</div>
);
}

103
src/components/InputBox.tsx Normal file
View File

@ -0,0 +1,103 @@
import React from 'react';
import {ThemeProps, themeable} from '../theme';
import uncontrollable from 'uncontrollable';
import Input from './Input';
import {autobind} from '../utils/helper';
import {Icon} from './icons';
export interface InputBoxProps
extends ThemeProps,
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
value?: string;
onChange?: (value: string) => void;
clearable?: boolean;
disabled?: boolean;
placeholder?: string;
children?: JSX.Element;
}
export interface InputBoxState {
isFocused: boolean;
}
export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
static defaultProps = {
clearable: true,
placeholder: ''
};
state = {
isFocused: false
};
@autobind
clearValue() {
const onChange = this.props.onChange;
onChange && onChange('');
}
@autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const onChange = this.props.onChange;
onChange && onChange(e.currentTarget.value);
}
@autobind
handleFocus(e: any) {
const onFocus = this.props.onFocus;
onFocus && onFocus(e);
this.setState({
isFocused: true
});
}
@autobind
handleBlur(e: any) {
const onBlur = this.props.onBlur;
onBlur && onBlur(e);
this.setState({
isFocused: false
});
}
render() {
const {
className,
classnames: cx,
classPrefix,
clearable,
disabled,
value,
placeholder,
children,
...rest
} = this.props;
const isFocused = this.state.isFocused;
return (
<div className={cx('InputBox', className, isFocused ? 'is-focused' : '')}>
<Input
{...rest}
value={value || ''}
onChange={this.handleChange}
placeholder={placeholder}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
{clearable && !disabled && value ? (
<a onClick={this.clearValue} className={cx('InputBox-clear')}>
<Icon icon="close" className="icon" />
</a>
) : null}
{children}
</div>
);
}
}
export default themeable(
uncontrollable(InputBox, {
value: 'onChange'
})
);

339
src/components/Transfer.tsx Normal file
View File

@ -0,0 +1,339 @@
import React from 'react';
import {ThemeProps, themeable} from '../theme';
import {CheckboxesProps, Checkboxes} from './Checkboxes';
import {Options} from './Select';
import uncontrollable from 'uncontrollable';
import Selections from './Selections';
import TableCheckboxes from './TableCheckboxes';
import ListCheckboxes from './ListCheckboxes';
import TreeCheckboxes from './TreeCheckboxes';
import {autobind, flattenTree} from '../utils/helper';
import InputBox from './InputBox';
import {Icon} from './icons';
import debounce from 'lodash/debounce';
export interface TransferPorps extends ThemeProps, CheckboxesProps {
inline?: boolean;
selectTitle: string;
selectMode?: 'table' | 'list' | 'tree';
columns?: Array<{
name: string;
label: string;
[propName: string]: any;
}>;
// search 相关
searchResultMode?: 'table' | 'list' | 'tree';
searchResultColumns?: Array<{
name: string;
label: string;
[propName: string]: any;
}>;
searchPlaceholder?: string;
noResultsText?: string;
onSearch?: (
term: string,
setCancel: (cancel: () => void) => void
) => Promise<Options>;
// 自定义选择框相关
selectRender?: (props: TransferPorps) => JSX.Element;
resultTitle: string;
sortable?: boolean;
}
export interface TransferState {
inputValue: string;
searchResult: Options | null;
}
export class Transfer extends React.Component<TransferPorps, TransferState> {
static defaultProps = {
selectTitle: '请选择',
resultTitle: '当前选择'
};
state = {
inputValue: '',
searchResult: null
};
valueArray: Options;
availableOptions: Options;
unmounted = false;
cancelSearch?: () => void;
componentWillUnmount() {
this.lazySearch.cancel();
this.unmounted = true;
}
@autobind
toggleAll() {
const {options, option2value, onChange, value} = this.props;
let valueArray = Checkboxes.value2array(value, options, option2value);
const availableOptions = flattenTree(options).filter(
option => !option.disabled && option.value !== void 0
);
if (valueArray.length < availableOptions.length) {
valueArray = availableOptions;
} else {
valueArray = [];
}
let newValue: string | Options = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
}
@autobind
clearAll() {
const {onChange} = this.props;
onChange && onChange([]);
}
@autobind
handleSearch(text: string) {
this.setState(
{
inputValue: text
},
() => {
// 如果有取消搜索,先取消掉。
this.cancelSearch && this.cancelSearch();
this.lazySearch(text);
}
);
}
@autobind
handleSeachCancel() {
this.setState({
inputValue: '',
searchResult: null
});
}
lazySearch = debounce(
async (text: string) => {
const onSearch = this.props.onSearch!;
let result = await onSearch(
text,
(cancel: () => void) => (this.cancelSearch = cancel)
);
if (this.unmounted) {
return;
}
if (!Array.isArray(result)) {
throw new Error('onSearch 需要返回数组');
}
this.setState({
searchResult: result
});
},
250,
{
trailing: true,
leading: false
}
);
renderSelect() {
const {
selectRender,
selectMode,
classnames: cx,
selectTitle,
onSearch
} = this.props;
if (selectRender) {
return selectRender(this.props);
}
return (
<>
<div
className={cx(
'Transfer-title',
selectMode === 'table' ? 'Transfer-title--light' : ''
)}
>
<span>
{selectTitle}{this.valueArray.length}/
{this.availableOptions.length}
</span>
{selectMode !== 'table' ? (
<a onClick={this.toggleAll} className={cx('Transfer-checkAll')}>
</a>
) : null}
</div>
{onSearch ? (
<div className={cx('Transfer-search')}>
<InputBox
value={this.state.inputValue}
onChange={this.handleSearch}
placeholder="请输入关键字"
clearable={false}
>
{this.state.searchResult !== null ? (
<a onClick={this.handleSeachCancel}>
<Icon icon="close" className="icon" />
</a>
) : (
<Icon icon="search" className="icon" />
)}
</InputBox>
</div>
) : null}
{this.state.searchResult !== null
? this.renderSearchResult()
: this.renderOptions()}
</>
);
}
renderSearchResult() {
const {
searchResultMode,
noResultsText,
searchResultColumns,
classnames: cx,
value,
onChange,
option2value
} = this.props;
const options = this.state.searchResult || [];
return searchResultMode === 'table' ? (
<TableCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
columns={searchResultColumns!}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : searchResultMode === 'tree' ? (
<TreeCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : (
<ListCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
);
}
renderOptions() {
const {
selectMode,
columns,
options,
value,
onChange,
option2value,
classnames: cx
} = this.props;
return selectMode === 'table' ? (
<TableCheckboxes
className={cx('Transfer-checkboxes')}
columns={columns!}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : selectMode === 'tree' ? (
<TreeCheckboxes
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : (
<ListCheckboxes
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
);
}
render() {
const {
inline,
classnames: cx,
className,
value,
onChange,
resultTitle,
sortable,
options,
option2value
} = this.props;
this.valueArray = Checkboxes.value2array(value, options, option2value);
this.availableOptions = flattenTree(options).filter(
option => !option.disabled && option.value !== void 0
);
return (
<div
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
>
<div className={cx('Transfer-select')}>{this.renderSelect()}</div>
<div className={cx('Transfer-arrow')}></div>
<div className={cx('Transfer-result')}>
<div className={cx('Transfer-title')}>
<span>
{resultTitle}{this.valueArray.length}/
{this.availableOptions.length}
</span>
<a onClick={this.clearAll} className={cx('Transfer-clearAll')}>
</a>
</div>
<Selections
className={cx('Transfer-selections')}
sortable={sortable}
value={value}
onChange={onChange}
/>
</div>
</div>
);
}
}
export default themeable(
uncontrollable(Transfer, {
value: 'onChange'
})
);

View File

@ -4,6 +4,7 @@ import React from 'react';
import uncontrollable from 'uncontrollable';
import Checkbox from './Checkbox';
import {Option} from './Select';
import {autobind} from '../utils/helper';
export interface TreeCheckboxesProps extends CheckboxesProps {}

View File

@ -743,10 +743,12 @@ export const detectProps = [
export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
return (Control: FormControlComponent) => {
const isSFC = !(Control.prototype instanceof React.Component);
// 兼容老的 FormItem 用法。
if (config.validate && !Control.prototype.validate) {
const fn = config.validate;
Control.prototype.validate = function() {
Control.prototype.validate = function () {
// console.warn('推荐直接在类中定义,而不是 FormItem HOC 的参数中传入。');
const host = {
input: this
@ -855,7 +857,7 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
onBlur={this.handleBlur}
type={type}
classnames={cx}
ref={this.refFn}
ref={isSFC ? undefined : this.refFn}
formItem={model}
className={cx(
`Form-control`,
@ -901,7 +903,7 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
}
export function FormItem(config: FormItemBasicConfig) {
return function(component: FormControlComponent): any {
return function (component: FormControlComponent): any {
const renderer = registerFormItem({
...config,
component