index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
  4. import PropTypes from 'prop-types';
  5. import { isEqual, noop, omit, isEmpty, isArray } from 'lodash-es';
  6. import TransferFoundation, { TransferAdapter, BasicDataItem, OnSortEndProps } from '@douyinfe/semi-foundation/transfer/foundation';
  7. import { _generateDataByType, _generateSelectedItems } from '@douyinfe/semi-foundation/transfer/transferUtlls';
  8. import { cssClasses, strings } from '@douyinfe/semi-foundation/transfer/constants';
  9. import '@douyinfe/semi-foundation/transfer/transfer.scss';
  10. import BaseComponent from '../_base/baseComponent';
  11. import LocaleConsumer from '../locale/localeConsumer';
  12. import { Locale } from '../locale/interface';
  13. import { Checkbox } from '../checkbox/index';
  14. import Input, { InputProps } from '../input/index';
  15. import Spin from '../spin';
  16. import Button from '../button';
  17. import Tree from '../tree';
  18. import { IconClose, IconSearch, IconHandle } from '@douyinfe/semi-icons';
  19. import { Value as TreeValue, TreeProps } from '../tree/interface';
  20. export interface DataItem extends BasicDataItem {
  21. label?: React.ReactNode;
  22. style?: React.CSSProperties;
  23. }
  24. export interface GroupItem {
  25. title?: string;
  26. children?: Array<DataItem>;
  27. }
  28. export interface TreeItem extends DataItem {
  29. children: Array<TreeItem>;
  30. }
  31. export interface RenderSourceItemProps extends DataItem {
  32. checked: boolean;
  33. onChange?: () => void;
  34. }
  35. export interface RenderSelectedItemProps extends DataItem {
  36. onRemove?: () => void;
  37. sortableHandle?: typeof SortableHandle;
  38. }
  39. export interface EmptyContent {
  40. left?: React.ReactNode;
  41. right?: React.ReactNode;
  42. search?: React.ReactNode;
  43. }
  44. export type Type = 'list' | 'groupList' | 'treeList';
  45. export interface SourcePanelProps {
  46. value: Array<string | number>;
  47. /* Loading */
  48. loading: boolean;
  49. /* Whether there are no items that match the current search value */
  50. noMatch: boolean;
  51. /* Items that match the current search value */
  52. filterData: Array<DataItem>;
  53. /* All items */
  54. sourceData: Array<DataItem>;
  55. /* Whether to select all */
  56. allChecked: boolean;
  57. /* Number of filtered results */
  58. showNumber: number;
  59. /* Input search box value */
  60. inputValue: string;
  61. /* The function that should be called when the search box changes */
  62. onSearch: (searchString: string) => void;
  63. /* The function that should be called when all the buttons on the left are clicked */
  64. onAllClick: () => void;
  65. /* Selected item on the left */
  66. selectedItems: Map<string | number, DataItem>;
  67. /* The function that should be called when selecting or deleting a single option */
  68. onSelectOrRemove: (item: DataItem) => void;
  69. /* The function that should be called when selecting an option, */
  70. onSelect: (value: Array<string | number>) => void;
  71. }
  72. export type OnSortEnd = ({ oldIndex, newIndex }: OnSortEndProps) => void;
  73. export interface SelectedPanelProps {
  74. /* Number of selected options */
  75. length: number;
  76. /* Collection of all selected options */
  77. selectedData: Array<DataItem>;
  78. /* Callback function that should be called when click to clear */
  79. onClear: () => void;
  80. /* The function that should be called when a single option is deleted */
  81. onRemove: (item: DataItem) => void;
  82. /* The function that should be called when reordering the results */
  83. onSortEnd: OnSortEnd;
  84. }
  85. export interface ResolvedDataItem extends DataItem {
  86. _parent?: {
  87. title: string;
  88. };
  89. _optionKey?: string | number;
  90. }
  91. export type DataSource = Array<DataItem> | Array<GroupItem> | Array<TreeItem>;
  92. interface HeaderConfig {
  93. totalContent: string;
  94. allContent: string;
  95. onAllClick: () => void;
  96. type: string;
  97. showButton: boolean;
  98. }
  99. export interface TransferState {
  100. data: Array<ResolvedDataItem>;
  101. selectedItems: Map<number | string, ResolvedDataItem>;
  102. searchResult: Set<number | string>;
  103. inputValue: string;
  104. }
  105. export interface TransferProps {
  106. style?: React.CSSProperties;
  107. className?: string;
  108. disabled?: boolean;
  109. dataSource?: DataSource;
  110. filter?: boolean | ((sugInput: string, item: DataItem) => boolean);
  111. defaultValue?: Array<string | number>;
  112. value?: Array<string | number>;
  113. inputProps?: InputProps;
  114. type?: Type;
  115. emptyContent?: EmptyContent;
  116. draggable?: boolean;
  117. treeProps?: Omit<TreeProps, 'value' | 'ref' | 'onChange'>;
  118. showPath?: boolean;
  119. loading?: boolean;
  120. onChange?: (values: Array<string | number>, items: Array<DataItem>) => void;
  121. onSelect?: (item: DataItem) => void;
  122. onDeselect?: (item: DataItem) => void;
  123. onSearch?: (sunInput: string) => void;
  124. renderSourceItem?: (item: RenderSourceItemProps) => React.ReactNode;
  125. renderSelectedItem?: (item: RenderSelectedItemProps) => React.ReactNode;
  126. renderSourcePanel?: (sourcePanelProps: SourcePanelProps) => React.ReactNode;
  127. renderSelectedPanel?: (selectedPanelProps: SelectedPanelProps) => React.ReactNode;
  128. }
  129. const prefixcls = cssClasses.PREFIX;
  130. class Transfer extends BaseComponent<TransferProps, TransferState> {
  131. static propTypes = {
  132. style: PropTypes.object,
  133. className: PropTypes.string,
  134. disabled: PropTypes.bool,
  135. dataSource: PropTypes.array,
  136. filter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  137. onSearch: PropTypes.func,
  138. inputProps: PropTypes.object,
  139. value: PropTypes.array,
  140. defaultValue: PropTypes.array,
  141. onChange: PropTypes.func,
  142. onSelect: PropTypes.func,
  143. onDeselect: PropTypes.func,
  144. renderSourceItem: PropTypes.func,
  145. renderSelectedItem: PropTypes.func,
  146. loading: PropTypes.bool,
  147. type: PropTypes.oneOf(['list', 'groupList', 'treeList']),
  148. treeProps: PropTypes.object,
  149. showPath: PropTypes.bool,
  150. emptyContent: PropTypes.shape({
  151. search: PropTypes.node,
  152. left: PropTypes.node,
  153. right: PropTypes.node,
  154. }),
  155. renderSourcePanel: PropTypes.func,
  156. renderSelectedPanel: PropTypes.func,
  157. draggable: PropTypes.bool,
  158. };
  159. static defaultProps = {
  160. type: strings.TYPE_LIST,
  161. dataSource: [] as DataSource,
  162. onSearch: noop,
  163. onChange: noop,
  164. onSelect: noop,
  165. onDeselect: noop,
  166. onClear: noop,
  167. defaultValue: [] as Array<string | number>,
  168. emptyContent: {},
  169. showPath: false,
  170. };
  171. _treeRef: React.RefObject<Tree> = null;
  172. constructor(props: TransferProps) {
  173. super(props);
  174. const { defaultValue = [], dataSource, type } = props;
  175. this.foundation = new TransferFoundation<TransferProps, TransferState>(this.adapter);
  176. this.state = {
  177. data: [],
  178. selectedItems: new Map(),
  179. searchResult: new Set(),
  180. inputValue: '',
  181. };
  182. if (Boolean(dataSource) && isArray(dataSource)) {
  183. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  184. // @ts-ignore Avoid reporting errors this.state.xxx is read-only
  185. this.state.data = _generateDataByType(dataSource, type);
  186. }
  187. if (Boolean(defaultValue) && isArray(defaultValue)) {
  188. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  189. // @ts-ignore Avoid reporting errors this.state.xxx is read-only
  190. this.state.selectedItems = _generateSelectedItems(defaultValue, this.state.data);
  191. }
  192. this.onSelectOrRemove = this.onSelectOrRemove.bind(this);
  193. this.onInputChange = this.onInputChange.bind(this);
  194. this.onSortEnd = this.onSortEnd.bind(this);
  195. }
  196. static getDerivedStateFromProps(props: TransferProps, state: TransferState) {
  197. const { value, dataSource, type, filter } = props;
  198. const mergedState = {} as TransferState;
  199. let newData = state.data;
  200. let newSelectedItems = state.selectedItems;
  201. if (Boolean(dataSource) && Array.isArray(dataSource)) {
  202. newData = _generateDataByType(dataSource, type);
  203. mergedState.data = newData;
  204. }
  205. if (Boolean(value) && Array.isArray(value)) {
  206. newSelectedItems = _generateSelectedItems(value, newData);
  207. mergedState.selectedItems = newSelectedItems;
  208. }
  209. if (!isEqual(state.data, newData)) {
  210. if (typeof state.inputValue === 'string' && state.inputValue !== '') {
  211. const filterFunc = typeof filter === 'function' ?
  212. (item: DataItem) => filter(state.inputValue, item) :
  213. (item: DataItem) => typeof item.label === 'string' && item.label.includes(state.inputValue);
  214. const searchData = newData.filter(filterFunc);
  215. const searchResult = new Set(searchData.map(item => item.key));
  216. mergedState.searchResult = searchResult;
  217. }
  218. }
  219. return isEmpty(mergedState) ? null : mergedState;
  220. }
  221. get adapter(): TransferAdapter<TransferProps, TransferState> {
  222. return {
  223. ...super.adapter,
  224. getSelected: () => new Map(this.state.selectedItems),
  225. updateSelected: selectedItems => {
  226. this.setState({ selectedItems });
  227. },
  228. notifyChange: (values, items) => {
  229. this.props.onChange(values, items);
  230. },
  231. notifySearch: input => {
  232. this.props.onSearch(input);
  233. },
  234. notifySelect: item => {
  235. this.props.onSelect(item);
  236. },
  237. notifyDeselect: item => {
  238. this.props.onDeselect(item);
  239. },
  240. updateInput: input => {
  241. this.setState({ inputValue: input });
  242. },
  243. updateSearchResult: searchResult => {
  244. this.setState({ searchResult });
  245. },
  246. searchTree: keyword => {
  247. this._treeRef && (this._treeRef as any).search(keyword); // TODO check this._treeRef.current?
  248. }
  249. };
  250. }
  251. onInputChange(value: string) {
  252. this.foundation.handleInputChange(value);
  253. }
  254. onSelectOrRemove(item: ResolvedDataItem) {
  255. this.foundation.handleSelectOrRemove(item);
  256. }
  257. onSortEnd(callbackProps: OnSortEndProps) {
  258. this.foundation.handleSortEnd(callbackProps);
  259. }
  260. renderFilter(locale: Locale['Transfer']) {
  261. const { inputProps, filter, disabled } = this.props;
  262. if (typeof filter === 'boolean' && !filter) {
  263. return null;
  264. }
  265. return (
  266. <div className={`${prefixcls }-filter`}>
  267. <Input
  268. prefix={<IconSearch />}
  269. placeholder={locale.placeholder}
  270. showClear
  271. value={this.state.inputValue}
  272. disabled={disabled}
  273. onChange={this.onInputChange}
  274. {...inputProps}
  275. />
  276. </div>
  277. );
  278. }
  279. renderHeader(headerConfig: HeaderConfig) {
  280. const { disabled } = this.props;
  281. const { totalContent, allContent, onAllClick, type, showButton } = headerConfig;
  282. const headerCls = cls({
  283. [`${prefixcls }-header`]: true,
  284. [`${prefixcls }-right-header`]: type === 'right',
  285. [`${prefixcls }-left-header`]: type === 'left',
  286. });
  287. return (
  288. <div className={headerCls}>
  289. <span className={`${prefixcls }-header-total`}>{totalContent}</span>
  290. {showButton ? (
  291. <Button
  292. theme="borderless"
  293. disabled={disabled}
  294. type="tertiary"
  295. size="small"
  296. className={`${prefixcls }-header-all`}
  297. onClick={onAllClick}
  298. >
  299. {allContent}
  300. </Button>
  301. ) : null}
  302. </div>
  303. );
  304. }
  305. renderLeftItem(item: ResolvedDataItem, index: number) {
  306. const { renderSourceItem, disabled } = this.props;
  307. const { selectedItems } = this.state;
  308. const checked = selectedItems.has(item.key);
  309. if (renderSourceItem) {
  310. return renderSourceItem({ ...item, checked, onChange: () => this.onSelectOrRemove(item) });
  311. }
  312. const leftItemCls = cls({
  313. [`${prefixcls }-item`]: true,
  314. [`${prefixcls }-item-disabled`]: item.disabled,
  315. });
  316. return (
  317. <Checkbox
  318. key={index}
  319. disabled={item.disabled || disabled}
  320. className={leftItemCls}
  321. checked={checked}
  322. onChange={() => this.onSelectOrRemove(item)}
  323. >
  324. {item.label}
  325. </Checkbox>
  326. );
  327. }
  328. renderLeft(locale: Locale['Transfer']) {
  329. const { data, selectedItems, inputValue, searchResult } = this.state;
  330. const { loading, type, emptyContent, renderSourcePanel } = this.props;
  331. const totalToken = locale.total;
  332. const inSearchMode = inputValue !== '';
  333. const showNumber = inSearchMode ? searchResult.size : data.length;
  334. const filterData = inSearchMode ? data.filter(item => searchResult.has(item.key)) : data;
  335. // Whether to select all should be a judgment, whether the filtered data on the left is a subset of the selected items
  336. // For example, the filtered data on the left is 1, 3, 4;
  337. // The selected option is 1,2,3,4, it is true
  338. // The selected option is 2,3,4, then it is false
  339. const leftContainesNotInSelected = Boolean(filterData.find(f => !selectedItems.has(f.key)));
  340. const totalText = totalToken.replace('${total}', `${showNumber}`);
  341. const headerConfig: HeaderConfig = {
  342. totalContent: totalText,
  343. allContent: leftContainesNotInSelected ? locale.selectAll : locale.clearSelectAll,
  344. onAllClick: () => this.foundation.handleAll(leftContainesNotInSelected),
  345. type: 'left',
  346. showButton: type !== strings.TYPE_TREE_TO_LIST,
  347. };
  348. const inputCom = this.renderFilter(locale);
  349. const headerCom = this.renderHeader(headerConfig);
  350. const noMatch = inSearchMode && searchResult.size === 0;
  351. const emptySearch = emptyContent.search ? emptyContent.search : locale.emptySearch;
  352. const emptyLeft = emptyContent.left ? emptyContent.left : locale.emptyLeft;
  353. const emptyCom = this.renderEmpty('left', inputValue ? emptySearch : emptyLeft);
  354. const loadingCom = <Spin />;
  355. let content: React.ReactNode = null;
  356. switch (true) {
  357. case loading:
  358. content = loadingCom;
  359. break;
  360. case noMatch:
  361. content = emptyCom;
  362. break;
  363. case type === strings.TYPE_TREE_TO_LIST:
  364. content = (
  365. <>
  366. {headerCom}
  367. {this.renderLeftTree()}
  368. </>
  369. );
  370. break;
  371. case !noMatch && (type === strings.TYPE_LIST || type === strings.TYPE_GROUP_LIST):
  372. content = (
  373. <>
  374. {headerCom}
  375. {this.renderLeftList(filterData)}
  376. </>
  377. );
  378. break;
  379. default:
  380. content = null;
  381. break;
  382. }
  383. const { values } = this.foundation.getValuesAndItemsFromMap(selectedItems);
  384. const renderProps: SourcePanelProps = {
  385. loading,
  386. noMatch,
  387. filterData,
  388. sourceData: data,
  389. allChecked: !leftContainesNotInSelected,
  390. showNumber,
  391. inputValue,
  392. selectedItems,
  393. value: values,
  394. onSelect: this.foundation.handleSelect.bind(this.foundation),
  395. onAllClick: () => this.foundation.handleAll(leftContainesNotInSelected),
  396. onSearch: this.onInputChange,
  397. onSelectOrRemove: (item: ResolvedDataItem) => this.onSelectOrRemove(item),
  398. };
  399. if (renderSourcePanel) {
  400. return renderSourcePanel(renderProps);
  401. }
  402. return (
  403. <section className={`${prefixcls }-left`}>
  404. {inputCom}
  405. {content}
  406. </section>
  407. );
  408. }
  409. renderGroupTitle(group: GroupItem) {
  410. const groupCls = cls(`${prefixcls }-group-title`);
  411. return (
  412. <div className={groupCls} key={group.title}>
  413. {group.title}
  414. </div>
  415. );
  416. }
  417. renderLeftTree() {
  418. const { selectedItems } = this.state;
  419. const { disabled, dataSource, treeProps } = this.props;
  420. const { values } = this.foundation.getValuesAndItemsFromMap(selectedItems);
  421. const onChange = (value: TreeValue) => {
  422. this.foundation.handleSelect(value);
  423. };
  424. const restTreeProps = omit(treeProps, ['value', 'ref', 'onChange']);
  425. return (
  426. <Tree
  427. disabled={disabled}
  428. treeData={dataSource as any}
  429. multiple
  430. disableStrictly
  431. value={values}
  432. defaultExpandAll
  433. leafOnly
  434. ref={this._treeRef}
  435. filterTreeNode
  436. searchRender={false}
  437. searchStyle={{ padding: 0 }}
  438. style={{ flex: 1, overflow: 'overlay' }}
  439. onChange={onChange}
  440. {...restTreeProps}
  441. />
  442. );
  443. }
  444. renderLeftList(visibileItems: Array<ResolvedDataItem>) {
  445. const content = [] as Array<React.ReactNode>;
  446. const groupStatus = new Map();
  447. visibileItems.forEach((item, index) => {
  448. const parentGroup = item._parent;
  449. const optionContent = this.renderLeftItem(item, index);
  450. if (parentGroup && groupStatus.has(parentGroup.title)) {
  451. // group content already insert
  452. content.push(optionContent);
  453. } else if (parentGroup) {
  454. const groupContent = this.renderGroupTitle(parentGroup);
  455. groupStatus.set(parentGroup.title, true);
  456. content.push(groupContent);
  457. content.push(optionContent);
  458. } else {
  459. content.push(optionContent);
  460. }
  461. });
  462. return <div className={`${prefixcls }-left-list`}>{content}</div>;
  463. }
  464. renderRightItem(item: ResolvedDataItem): React.ReactNode {
  465. const { renderSelectedItem, draggable, type, showPath } = this.props;
  466. let newItem = item;
  467. if (draggable) {
  468. newItem = { ...item, key: item._optionKey };
  469. delete newItem._optionKey;
  470. }
  471. const onRemove = () => this.foundation.handleSelectOrRemove(newItem);
  472. const rightItemCls = cls({
  473. [`${prefixcls }-item`]: true,
  474. [`${prefixcls }-right-item`]: true,
  475. [`${prefixcls }-right-item-draggable`]: draggable
  476. });
  477. const shouldShowPath = type === strings.TYPE_TREE_TO_LIST && showPath === true;
  478. const label = shouldShowPath ? this.foundation._generatePath(item) : item.label;
  479. if (renderSelectedItem) {
  480. return renderSelectedItem({ ...item, onRemove, sortableHandle: SortableHandle });
  481. }
  482. const DragHandle = SortableHandle(() => (
  483. <IconHandle className={`${prefixcls }-right-item-drag-handler`} />
  484. ));
  485. return (
  486. <div className={rightItemCls} key={newItem.key}>
  487. {draggable ? <DragHandle /> : null}
  488. <div className={`${prefixcls}-right-item-text`}>{label}</div>
  489. <IconClose
  490. onClick={onRemove} className={cls(`${prefixcls}-item-close-icon`, {
  491. [`${prefixcls}-item-close-icon-disabled`]: item.disabled
  492. })}
  493. />
  494. </div>
  495. );
  496. }
  497. renderEmpty(type: string, emptyText: React.ReactNode) {
  498. const emptyCls = cls({
  499. [`${prefixcls }-empty`]: true,
  500. [`${prefixcls }-right-empty`]: type === 'right',
  501. [`${prefixcls }-left-empty`]: type === 'left',
  502. });
  503. return <div className={emptyCls}>{emptyText}</div>;
  504. }
  505. renderRightSortableList(selectedData: Array<ResolvedDataItem>) {
  506. // when choose some items && draggable is true
  507. const SortableItem = SortableElement((
  508. (item: ResolvedDataItem) => this.renderRightItem(item)) as React.SFC<ResolvedDataItem>
  509. );
  510. const SortableList = SortableContainer(({ items }: { items: Array<ResolvedDataItem> }) => (
  511. <div className={`${prefixcls}-right-list`}>
  512. {items.map((item, index: number) => (
  513. // sortableElement will take over the property 'key', so use another '_optionKey' to pass
  514. <SortableItem key={item.label} index={index} {...item} _optionKey={item.key} />
  515. ))}
  516. </div>
  517. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  518. // @ts-ignore see reasons: https://github.com/clauderic/react-sortable-hoc/issues/206
  519. ), { distance: 10 });
  520. const sortList = <SortableList useDragHandle onSortEnd={this.onSortEnd} items={selectedData} />;
  521. return sortList;
  522. }
  523. renderRight(locale: Locale['Transfer']) {
  524. const { selectedItems } = this.state;
  525. const { emptyContent, renderSelectedPanel, draggable } = this.props;
  526. const selectedData = [...selectedItems.values()];
  527. // when custom render panel
  528. const renderProps: SelectedPanelProps = {
  529. length: selectedData.length,
  530. selectedData,
  531. onClear: () => this.foundation.handleClear(),
  532. onRemove: item => this.foundation.handleSelectOrRemove(item),
  533. onSortEnd: props => this.onSortEnd(props)
  534. };
  535. if (renderSelectedPanel) {
  536. return renderSelectedPanel(renderProps);
  537. }
  538. const selectedToken = locale.selected;
  539. const selectedText = selectedToken.replace('${total}', `${selectedData.length}`);
  540. const headerConfig = {
  541. totalContent: selectedText,
  542. allContent: locale.clear,
  543. onAllClick: () => this.foundation.handleClear(),
  544. type: 'right',
  545. showButton: Boolean(selectedData.length),
  546. };
  547. const headerCom = this.renderHeader(headerConfig);
  548. const emptyCom = this.renderEmpty('right', emptyContent.right ? emptyContent.right : locale.emptyRight);
  549. const panelCls = `${prefixcls }-right`;
  550. let content = null;
  551. switch (true) {
  552. // when empty
  553. case !selectedData.length:
  554. content = emptyCom;
  555. break;
  556. case selectedData.length && !draggable:
  557. const list = (
  558. <div className={`${prefixcls }-right-list`}>
  559. {selectedData.map(item => this.renderRightItem({ ...item }))}
  560. </div>
  561. );
  562. content = list;
  563. break;
  564. case selectedData.length && draggable:
  565. content = this.renderRightSortableList(selectedData);
  566. break;
  567. default:
  568. break;
  569. }
  570. return (
  571. <section className={panelCls}>
  572. {headerCom}
  573. {content}
  574. </section>
  575. );
  576. }
  577. render() {
  578. const { className, style, disabled, renderSelectedPanel, renderSourcePanel } = this.props;
  579. const transferCls = cls(prefixcls, className, {
  580. [`${prefixcls }-disabled`]: disabled,
  581. [`${prefixcls }-custom-panel`]: renderSelectedPanel && renderSourcePanel,
  582. });
  583. return (
  584. <LocaleConsumer componentName="Transfer">
  585. {(locale: Locale['Transfer']) => (
  586. <div className={transferCls} style={style}>
  587. {this.renderLeft(locale)}
  588. {this.renderRight(locale)}
  589. </div>
  590. )}
  591. </LocaleConsumer>
  592. );
  593. }
  594. }
  595. export default Transfer;