diff --git a/scss/components/_array-input.scss b/scss/components/_array-input.scss new file mode 100644 index 00000000..73dc8950 --- /dev/null +++ b/scss/components/_array-input.scss @@ -0,0 +1,89 @@ +.#{$ns}ArrayInput { + &-placeholder { + color: $text--muted-color; + padding-top: $Form-label-paddingTop; + } + + &-addBtn { + > svg { + width: $Combo-addBtn-fontSize; + height: $Combo-addBtn-fontSize; + } + + font-size: $Combo-addBtn-fontSize; + + @include button-size( + $Combo-addBtn-paddingY, + $Combo-addBtn-paddingX, + $Combo-addBtn-fontSize, + $Combo-addBtn-lineHeight, + $Combo-addBtn-borderRadius, + $Combo-addBtn-height + ); + + @include button-variant( + $Combo-addBtn-bg, + $Combo-addBtn-border, + $Combo-addBtn-color, + $Combo-addBtn-onHover-bg, + $Combo-addBtn-onHover-border, + $Combo-addBtn-onHover-color, + $Combo-addBtn-onActive-bg, + $Combo-addBtn-onActive-border, + $Combo-addBtn-onActive-color + ); + + &.is-disabled { + pointer-events: none; + opacity: $Button-onDisabled-opacity; + } + } + + &-toolbar { + margin-top: $gap-sm; + } + + &-item { + display: flex; + flex-direction: row; + align-items: flex-start; + margin-bottom: $gap-sm; + + > div { + flex-grow: 1; + width: 0; + min-width: 120px; + margin-right: $gap-xs; + + &:not(:first-child) { + margin-left: $gap-xs; + } + } + + &--dragging { + position: relative; + opacity: 0.4; + } + } + + &-itemRemove, + &-itemDrager { + margin: 0; + flex-grow: unset; + display: inline-block; + padding: $Form-label-paddingTop $gap-xs; + cursor: pointer; + + > svg { + color: $icon-color; + } + + &:hover > svg { + color: $icon-onHover-color; + } + } + + &-itemDrager { + cursor: move; + } +} diff --git a/scss/components/form/_combo.scss b/scss/components/form/_combo.scss index 73c9382e..0b143103 100644 --- a/scss/components/form/_combo.scss +++ b/scss/components/form/_combo.scss @@ -1,7 +1,7 @@ .#{$ns}Combo { &-placeholder { color: $text--muted-color; - padding-top: px2rem(7px); + padding-top: $Form-label-paddingTop; } &-toolbarBtn { diff --git a/scss/themes/cxd.scss b/scss/themes/cxd.scss index cef1d2f5..aad7bce7 100644 --- a/scss/themes/cxd.scss +++ b/scss/themes/cxd.scss @@ -480,12 +480,14 @@ $Satus-icon-width: px2rem(14px); @import '../layout/aside'; @import '../layout/hbox'; @import '../layout/vbox'; +@import '../components/button'; // 这个要放在最前面 @import '../components/modal'; @import '../components/drawer'; @import '../components/tooltip'; @import '../components/popover'; @import '../components/toast'; @import '../components/alert'; +@import '../components/array-input'; @import '../components/tabs'; @import '../components/nav'; @import '../components/page'; @@ -496,7 +498,6 @@ $Satus-icon-width: px2rem(14px); @import '../components/panel'; @import '../components/service'; @import '../components/spinner'; -@import '../components/button'; @import '../components/button-group'; @import '../components/dropdown'; @import '../components/collapse'; diff --git a/scss/themes/dark.scss b/scss/themes/dark.scss index 323f7d45..0c030beb 100644 --- a/scss/themes/dark.scss +++ b/scss/themes/dark.scss @@ -166,12 +166,14 @@ pre { @import '../layout/aside'; @import '../layout/hbox'; @import '../layout/vbox'; +@import '../components/button'; // 这个要放在最前面 @import '../components/modal'; @import '../components/drawer'; @import '../components/tooltip'; @import '../components/popover'; @import '../components/toast'; @import '../components/alert'; +@import '../components/array-input'; @import '../components/tabs'; @import '../components/nav'; @import '../components/page'; @@ -182,7 +184,6 @@ pre { @import '../components/panel'; @import '../components/service'; @import '../components/spinner'; -@import '../components/button'; @import '../components/button-group'; @import '../components/dropdown'; @import '../components/collapse'; diff --git a/scss/themes/default.scss b/scss/themes/default.scss index b75ec9bd..607dfb29 100644 --- a/scss/themes/default.scss +++ b/scss/themes/default.scss @@ -29,12 +29,14 @@ $Form-input-borderColor: #cfdadd; @import '../layout/aside'; @import '../layout/hbox'; @import '../layout/vbox'; +@import '../components/button'; // 这个要放在最前面 @import '../components/modal'; @import '../components/drawer'; @import '../components/tooltip'; @import '../components/popover'; @import '../components/toast'; @import '../components/alert'; +@import '../components/array-input'; @import '../components/tabs'; @import '../components/nav'; @import '../components/page'; @@ -45,7 +47,6 @@ $Form-input-borderColor: #cfdadd; @import '../components/panel'; @import '../components/service'; @import '../components/spinner'; -@import '../components/button'; @import '../components/button-group'; @import '../components/dropdown'; @import '../components/collapse'; diff --git a/src/components/ArrayInput.tsx b/src/components/ArrayInput.tsx new file mode 100644 index 00000000..98e97a07 --- /dev/null +++ b/src/components/ArrayInput.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import {ThemeProps, themeable} from '../theme'; +import {LocaleProps, localeable} from '../locale'; +import InputBox from './InputBox'; +import {Icon} from './icons'; +import Button from './Button'; +import {autobind, guid} from '../utils/helper'; +import {uncontrollable} from 'uncontrollable'; +import Sortable from 'sortablejs'; +import {findDOMNode} from 'react-dom'; + +export interface ArrayInputProps extends ThemeProps, LocaleProps { + value?: Array; + onChange?: (value: Array) => void; + placeholder: string; + itemRender: ( + value: any, + onChange: (value: any) => void, + index: number, + disabled?: boolean + ) => JSX.Element; + itemInitalValue?: any; + maxLength?: number; + minLength?: number; + disabled?: boolean; + sortable?: boolean; + removable?: boolean; + addable?: boolean; + editable?: boolean; + sortTip?: string; +} + +export class ArrayInput extends React.Component { + static defaultProps = { + placeholder: '<空>', + itemRender: (value: any, onChange: (value: any) => void, index: number) => ( + + ) + }; + + id: string = guid(); + dragTip?: HTMLElement; + sortable?: Sortable; + + handleItemOnChange(index: number, itemValue: any) { + const {onChange} = this.props; + const value = this.props.value; + const newValue = Array.isArray(value) ? value.concat() : []; + newValue.splice(index, 1, itemValue); + onChange?.(newValue); + } + + @autobind + dragTipRef(ref: any) { + if (!this.dragTip && ref) { + this.initDragging(); + } else if (this.dragTip && !ref) { + this.destroyDragging(); + } + + this.dragTip = ref; + } + + @autobind + handleAdd() { + const {value, onChange, itemInitalValue} = this.props; + const newValue = Array.isArray(value) ? value.concat() : []; + + newValue.push(itemInitalValue); + + onChange?.(newValue); + } + + @autobind + handleRemove(e: React.MouseEvent) { + const indx = parseInt(e.currentTarget.getAttribute('data-index')!, 10); + const {value, onChange, itemInitalValue} = this.props; + const newValue = Array.isArray(value) ? value.concat() : []; + newValue.splice(indx, 1); + onChange?.(newValue); + } + + initDragging() { + const onChange = this.props.onChange; + const ns = this.props.classPrefix; + const dom = findDOMNode(this) as HTMLElement; + this.sortable = new Sortable( + dom.querySelector(`.drag-group`) as HTMLElement, + { + group: `array-input-${this.id}`, + animation: 150, + handle: `.drag-bar`, + ghostClass: `${ns}ArrayInput-item--dragging`, + onEnd: (e: any) => { + // 没有移动 + if (e.newIndex === e.oldIndex) { + return; + } + + // 换回来 + const parent = e.to as HTMLElement; + if ( + e.newIndex < e.oldIndex && + e.oldIndex < parent.childNodes.length - 1 + ) { + parent.insertBefore(e.item, parent.childNodes[e.oldIndex + 1]); + } else if (e.oldIndex < parent.childNodes.length - 1) { + parent.insertBefore(e.item, parent.childNodes[e.oldIndex]); + } else { + parent.appendChild(e.item); + } + + const value = this.props.value; + if (!Array.isArray(value)) { + return; + } + const newValue = value.concat(); + newValue.splice(e.newIndex, 0, newValue.splice(e.oldIndex, 1)[0]); + onChange?.(newValue); + } + } + ); + } + + destroyDragging() { + this.sortable && this.sortable.destroy(); + } + + renderItem(value: any, index: number, collection: Array) { + const { + itemRender, + disabled, + classnames: cx, + sortable, + removable, + minLength + } = this.props; + + return ( +
+ {sortable && collection.length > 1 && !disabled ? ( + + + + ) : null} + + {itemRender( + value, + this.handleItemOnChange.bind(this, index), + index, + disabled + )} + + {removable !== false && + !disabled && + (!minLength || collection.length > minLength) ? ( + + + + ) : null} +
+ ); + } + + render() { + const { + classnames: cx, + value, + placeholder, + translate: __, + maxLength, + sortable, + sortTip, + disabled + } = this.props; + + return ( +
+ {Array.isArray(value) && value.length ? ( +
+ {value.map((item, index) => this.renderItem(item, index, value))} +
+ ) : ( +
{__(placeholder)}
+ )} + +
+ {!Array.isArray(value) || !maxLength || value.length < maxLength ? ( + + ) : null} + + {sortable && Array.isArray(value) && value.length ? ( + + {Array.isArray(value) && value.length > 1 ? __(sortTip) : ''} + + ) : null} +
+
+ ); + } +} + +export default themeable( + localeable( + uncontrollable(ArrayInput, { + value: 'onChange' + }) + ) +); diff --git a/src/components/index.tsx b/src/components/index.tsx index 172c6848..574f5861 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -51,6 +51,7 @@ import ListRadios from './ListRadios'; import TreeRadios from './TreeRadios'; import ListGroup from './ListGroup'; import NumberInput from './NumberInput'; +import ArrayInput from './ArrayInput'; export { NotFound, @@ -104,5 +105,6 @@ export { ListRadios, TreeRadios, ListGroup, - NumberInput + NumberInput, + ArrayInput }; diff --git a/src/locale/en.ts b/src/locale/en.ts index 929c39b6..5f262c78 100644 --- a/src/locale/en.ts +++ b/src/locale/en.ts @@ -64,6 +64,7 @@ register('en', { '请输入街道信息': 'Enter street info', '删除': 'Delete', '新增': 'New', + '新增一条': 'Add a data', '新增一条数据': 'Add a data', '类型': 'Type', '拖拽排序': 'Drag to sort',