import React, { ReactNode } from 'react'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; import classNames from 'classnames'; import scrollIntoView from 'scroll-into-view-if-needed'; import ObserverCheckBox from '../check-box/CheckBox'; import { $l } from '../locale-context'; import Record from '../data-set/Record'; import ObserverTextField from '../text-field/TextField'; import Icon from '../icon'; import { DISABLED_FIELD, getItemKey, Select, SelectProps } from '../select/Select'; import autobind from '../_util/autobind'; import { stopPropagation } from '../_util/EventManager'; import ViewComponent from '../core/ViewComponent'; import CustomArea from './CustomArea'; export interface TransferListProps extends SelectProps { header?: ReactNode; selected: Record[]; currentIndex?: number; oneWay?: boolean; targetOption?: Record[]; direction: string; footer?: (options: Record[]) => ReactNode; onSelect: (e) => void; onSelectAll: (value: any) => void; setTargetOption?: (value: any) => void; onRemove?: (e) => void; } @observer export default class TransferList extends Select<TransferListProps> { wrapperRef: HTMLDivElement | null = null; get popup() { return true; } @computed get header(): ReactNode { const { prefixCls, multiple, observableProps: { header }, } = this; if (multiple || header) { return ( <div className={`${prefixCls}-header`}> {this.getHeaderSelected()} {header && <span className={`${prefixCls}-header-title`}>{header}</span>} </div> ); } return undefined; } @computed get footer(): ReactNode { const { prefixCls, filteredOptions, observableProps: { footer }, } = this; if (footer) { return <div className={`${prefixCls}-footer`}>{footer(filteredOptions)}</div>; } return undefined; } componentDidUpdate() { const { currentIndex } = this.props; // 在渲染完之后执行 if (this.wrapperRef && typeof currentIndex !== 'undefined') { const contentDom = this.wrapperRef.getElementsByTagName('ul')[0]; const findSelectedDom = this.wrapperRef.getElementsByTagName('li'); if (contentDom && currentIndex && currentIndex > -1 && currentIndex < this.filteredOptions.length) { const selectedDom = findSelectedDom[currentIndex] as HTMLLIElement; scrollIntoView(selectedDom, { block: 'end', behavior: 'smooth', scrollMode: 'if-needed', boundary: contentDom }); } this.options.setState('targetFilteredOptions', this.filteredOptions); } } getOmitPropsKeys(): string[] { return super .getOmitPropsKeys() .concat(['autoComplete', 'footer', 'header', 'selected', 'onSelect', 'onSelectAll']); } getOtherProps() { const otherProps = super.getOtherProps(); delete otherProps.ref; delete otherProps.type; delete otherProps.onChange; delete otherProps.onKeyDown; delete otherProps.currentIndex; delete otherProps.sortable; delete otherProps.sortOperations; delete otherProps.oneWay; delete otherProps.onRemove; delete otherProps.targetOption; delete otherProps.direction; return otherProps; } getObservableProps(props, context) { return { ...super.getObservableProps(props, context), header: props.header, footer: props.footer, }; } getMenuPrefixCls() { return `${this.prefixCls}-content`; } @autobind handleSelectAllChange(value) { const { onSelectAll } = this.props; if (onSelectAll) { onSelectAll(value ? this.filteredOptions.filter(record => !record.get(DISABLED_FIELD)) : []); } } @autobind handleClear() { this.setText(undefined); } getHeaderSelected() { const { filteredOptions: { length }, multiple, prefixCls, props: { selected: { length: selectedLength }, }, } = this; const selectedText = selectedLength ? `${selectedLength}/` : ''; const disabledLength = this.filteredOptions.filter(record => record.get(DISABLED_FIELD)).length; if (multiple) { return ( <ObserverCheckBox disabled={this.disabled} onChange={this.handleSelectAllChange} onFocus={stopPropagation} checked={!!length && disabledLength !== length && (length === selectedLength || length - disabledLength === selectedLength)} indeterminate={!!selectedLength && (length - disabledLength !== selectedLength)} > <span className={`${prefixCls}-header-selected`}> {`${selectedText}${length}${$l('Transfer', 'items')}`} </span> </ObserverCheckBox> ); } return ( <span className={`${prefixCls}-header-selected`}> {`${length}${$l('Transfer', 'items')}`} </span> ) } getSearchField(): ReactNode { const { prefixCls } = this; return ( <div className={`${prefixCls}-body-search-wrapper`}> <ObserverTextField ref={this.elementReference} onInput={this.handleChange} onClear={this.handleClear} onKeyDown={this.handleKeyDown} prefix={<Icon type="search" />} clearButton /> </div> ); } handleRemove = (value) => { const { onRemove } = this.props; if (onRemove) { onRemove(value); } } getMenuItem({ record, text, value }): string | ReactNode { const { prefixCls, multiple, options, props: { oneWay, optionRenderer }, } = this; if (oneWay && !multiple) { const renderer: ReactNode = ( <div className={`${prefixCls}-item-delete`}> <div>{text}</div> <Icon type="delete_black-o" className={`${prefixCls}-item-icon`} onClick={() => this.handleRemove(value)} /> </div> ); return optionRenderer ? optionRenderer({ dataSet: options, record, text, value }) : renderer } return super.getMenuItem({ record, text, value }) } renderBody(): ReactNode { const { prefixCls, searchable, textField, valueField, multiple, props: { selected, onSelect, onSelectAll, setTargetOption, children, targetOption, direction }, } = this; const searchField = searchable && this.getSearchField(); const classString = classNames(`${prefixCls}-body`, { [`${prefixCls}-body-with-search`]: searchable, }); // 自定义渲染 if (typeof children === 'function') { return ( <CustomArea targetOption={targetOption}>{ children({ direction, targetOption, setTargetOption, onItemSelect: onSelectAll })} </CustomArea> ); } const selectedKeys = multiple ? selected.map(record => getItemKey(record, record.get(textField), record.get(valueField)), ) : []; return ( <div className={classString}> {searchField} <div className={`${prefixCls}-content-wrapper`} ref={dom => { this.wrapperRef = dom; }} onFocus={searchable ? stopPropagation : undefined} > {this.getMenu({ selectedKeys, onClick: onSelect, focusable: !this.searchable })} </div> </div> ); } getClassName() { const { prefixCls, header, footer } = this; return super.getClassName({ [`${prefixCls}-with-header`]: header, [`${prefixCls}-with-footer`]: footer, }); } removeLastValue() { // noop } @autobind handleBlur(e) { ViewComponent.prototype.handleBlur.call(this, e); } renderHelpMessage(): ReactNode { return null; } renderWrapper() { const { header, footer, props: { style, children } } = this; const isCustom = typeof children === 'function'; return ( <div {...this.getOtherProps()} style={style}> {!isCustom && header} {this.renderBody()} {!isCustom && footer} </div> ); } }