1
0

index.tsx 26 KB

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