index.tsx 26 KB

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