list.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. "use client";
  2. import React, { useMemo, useRef, useState } from 'react';
  3. import DownOutlined from "@ant-design/icons/es/icons/DownOutlined";
  4. import classNames from 'classnames';
  5. import omit from "rc-util/es/omit";
  6. import { groupKeysMap } from '../_util/transKeys';
  7. import Checkbox from '../checkbox';
  8. import Dropdown from '../dropdown';
  9. import DefaultListBody, { OmitProps } from './ListBody';
  10. import Search from './search';
  11. const defaultRender = () => null;
  12. function isRenderResultPlainObject(result) {
  13. return !!(result && ! /*#__PURE__*/React.isValidElement(result) && Object.prototype.toString.call(result) === '[object Object]');
  14. }
  15. function getEnabledItemKeys(items) {
  16. return items.filter(data => !data.disabled).map(data => data.key);
  17. }
  18. const isValidIcon = icon => icon !== undefined;
  19. const getShowSearchOption = showSearch => {
  20. if (showSearch && typeof showSearch === 'object') {
  21. return Object.assign(Object.assign({}, showSearch), {
  22. defaultValue: showSearch.defaultValue || ''
  23. });
  24. }
  25. return {
  26. defaultValue: '',
  27. placeholder: ''
  28. };
  29. };
  30. const TransferList = props => {
  31. const {
  32. prefixCls,
  33. dataSource = [],
  34. titleText = '',
  35. checkedKeys,
  36. disabled,
  37. showSearch = false,
  38. style,
  39. searchPlaceholder,
  40. notFoundContent,
  41. selectAll,
  42. deselectAll,
  43. selectCurrent,
  44. selectInvert,
  45. removeAll,
  46. removeCurrent,
  47. showSelectAll = true,
  48. showRemove,
  49. pagination,
  50. direction,
  51. itemsUnit,
  52. itemUnit,
  53. selectAllLabel,
  54. selectionsIcon,
  55. footer,
  56. renderList,
  57. onItemSelectAll,
  58. onItemRemove,
  59. handleFilter,
  60. handleClear,
  61. filterOption,
  62. render = defaultRender
  63. } = props;
  64. const searchOptions = getShowSearchOption(showSearch);
  65. const [filterValue, setFilterValue] = useState(searchOptions.defaultValue);
  66. const listBodyRef = useRef({});
  67. const internalHandleFilter = e => {
  68. setFilterValue(e.target.value);
  69. handleFilter(e);
  70. };
  71. const internalHandleClear = () => {
  72. setFilterValue('');
  73. handleClear();
  74. };
  75. const matchFilter = (text, item) => {
  76. if (filterOption) {
  77. return filterOption(filterValue, item, direction);
  78. }
  79. return text.includes(filterValue);
  80. };
  81. const renderListBody = listProps => {
  82. let bodyContent = renderList ? renderList(Object.assign(Object.assign({}, listProps), {
  83. onItemSelect: (key, check) => listProps.onItemSelect(key, check)
  84. })) : null;
  85. const customize = !!bodyContent;
  86. if (!customize) {
  87. // @ts-ignore
  88. bodyContent = /*#__PURE__*/React.createElement(DefaultListBody, Object.assign({
  89. ref: listBodyRef
  90. }, listProps));
  91. }
  92. return {
  93. customize,
  94. bodyContent
  95. };
  96. };
  97. const renderItem = item => {
  98. const renderResult = render(item);
  99. const isRenderResultPlain = isRenderResultPlainObject(renderResult);
  100. return {
  101. item,
  102. renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
  103. renderedText: isRenderResultPlain ? renderResult.value : renderResult
  104. };
  105. };
  106. const notFoundContentEle = useMemo(() => Array.isArray(notFoundContent) ? notFoundContent[direction === 'left' ? 0 : 1] : notFoundContent, [notFoundContent, direction]);
  107. const [filteredItems, filteredRenderItems] = useMemo(() => {
  108. const filterItems = [];
  109. const filterRenderItems = [];
  110. dataSource.forEach(item => {
  111. const renderedItem = renderItem(item);
  112. if (filterValue && !matchFilter(renderedItem.renderedText, item)) {
  113. return;
  114. }
  115. filterItems.push(item);
  116. filterRenderItems.push(renderedItem);
  117. });
  118. return [filterItems, filterRenderItems];
  119. }, [dataSource, filterValue]);
  120. const checkedActiveItems = useMemo(() => {
  121. return filteredItems.filter(item => checkedKeys.includes(item.key) && !item.disabled);
  122. }, [checkedKeys, filteredItems]);
  123. const checkStatus = useMemo(() => {
  124. if (checkedActiveItems.length === 0) {
  125. return 'none';
  126. }
  127. const checkedKeysMap = groupKeysMap(checkedKeys);
  128. if (filteredItems.every(item => checkedKeysMap.has(item.key) || !!item.disabled)) {
  129. return 'all';
  130. }
  131. return 'part';
  132. }, [checkedKeys, checkedActiveItems]);
  133. const listBody = useMemo(() => {
  134. const search = showSearch ? (/*#__PURE__*/React.createElement("div", {
  135. className: `${prefixCls}-body-search-wrapper`
  136. }, /*#__PURE__*/React.createElement(Search, {
  137. prefixCls: `${prefixCls}-search`,
  138. onChange: internalHandleFilter,
  139. handleClear: internalHandleClear,
  140. placeholder: searchOptions.placeholder || searchPlaceholder,
  141. value: filterValue,
  142. disabled: disabled
  143. }))) : null;
  144. const {
  145. customize,
  146. bodyContent
  147. } = renderListBody(Object.assign(Object.assign({}, omit(props, OmitProps)), {
  148. filteredItems,
  149. filteredRenderItems,
  150. selectedKeys: checkedKeys
  151. }));
  152. let bodyNode;
  153. // We should wrap customize list body in a classNamed div to use flex layout.
  154. if (customize) {
  155. bodyNode = /*#__PURE__*/React.createElement("div", {
  156. className: `${prefixCls}-body-customize-wrapper`
  157. }, bodyContent);
  158. } else {
  159. bodyNode = filteredItems.length ? bodyContent : (/*#__PURE__*/React.createElement("div", {
  160. className: `${prefixCls}-body-not-found`
  161. }, notFoundContentEle));
  162. }
  163. return /*#__PURE__*/React.createElement("div", {
  164. className: classNames(`${prefixCls}-body`, {
  165. [`${prefixCls}-body-with-search`]: showSearch
  166. })
  167. }, search, bodyNode);
  168. }, [showSearch, prefixCls, searchPlaceholder, filterValue, disabled, checkedKeys, filteredItems, filteredRenderItems, notFoundContentEle]);
  169. const checkBox = /*#__PURE__*/React.createElement(Checkbox, {
  170. disabled: dataSource.filter(d => !d.disabled).length === 0 || disabled,
  171. checked: checkStatus === 'all',
  172. indeterminate: checkStatus === 'part',
  173. className: `${prefixCls}-checkbox`,
  174. onChange: () => {
  175. // Only select enabled items
  176. onItemSelectAll === null || onItemSelectAll === void 0 ? void 0 : onItemSelectAll(filteredItems.filter(item => !item.disabled).map(({
  177. key
  178. }) => key), checkStatus !== 'all');
  179. }
  180. });
  181. const getSelectAllLabel = (selectedCount, totalCount) => {
  182. if (selectAllLabel) {
  183. return typeof selectAllLabel === 'function' ? selectAllLabel({
  184. selectedCount,
  185. totalCount
  186. }) : selectAllLabel;
  187. }
  188. const unit = totalCount > 1 ? itemsUnit : itemUnit;
  189. return /*#__PURE__*/React.createElement(React.Fragment, null, (selectedCount > 0 ? `${selectedCount}/` : '') + totalCount, " ", unit);
  190. };
  191. // Custom Layout
  192. const footerDom = footer && (footer.length < 2 ? footer(props) : footer(props, {
  193. direction
  194. }));
  195. const listCls = classNames(prefixCls, {
  196. [`${prefixCls}-with-pagination`]: !!pagination,
  197. [`${prefixCls}-with-footer`]: !!footerDom
  198. });
  199. // ====================== Get filtered, checked item list ======================
  200. const listFooter = footerDom ? /*#__PURE__*/React.createElement("div", {
  201. className: `${prefixCls}-footer`
  202. }, footerDom) : null;
  203. const checkAllCheckbox = !showRemove && !pagination && checkBox;
  204. let items;
  205. if (showRemove) {
  206. items = [/* Remove Current Page */
  207. pagination ? {
  208. key: 'removeCurrent',
  209. label: removeCurrent,
  210. onClick() {
  211. var _a;
  212. const pageKeys = getEnabledItemKeys((((_a = listBodyRef.current) === null || _a === void 0 ? void 0 : _a.items) || []).map(entity => entity.item));
  213. onItemRemove === null || onItemRemove === void 0 ? void 0 : onItemRemove(pageKeys);
  214. }
  215. } : null, /* Remove All */
  216. {
  217. key: 'removeAll',
  218. label: removeAll,
  219. onClick() {
  220. onItemRemove === null || onItemRemove === void 0 ? void 0 : onItemRemove(getEnabledItemKeys(filteredItems));
  221. }
  222. }].filter(Boolean);
  223. } else {
  224. items = [{
  225. key: 'selectAll',
  226. label: checkStatus === 'all' ? deselectAll : selectAll,
  227. onClick() {
  228. const keys = getEnabledItemKeys(filteredItems);
  229. onItemSelectAll === null || onItemSelectAll === void 0 ? void 0 : onItemSelectAll(keys, keys.length !== checkedKeys.length);
  230. }
  231. }, pagination ? {
  232. key: 'selectCurrent',
  233. label: selectCurrent,
  234. onClick() {
  235. var _a;
  236. const pageItems = ((_a = listBodyRef.current) === null || _a === void 0 ? void 0 : _a.items) || [];
  237. onItemSelectAll === null || onItemSelectAll === void 0 ? void 0 : onItemSelectAll(getEnabledItemKeys(pageItems.map(entity => entity.item)), true);
  238. }
  239. } : null, {
  240. key: 'selectInvert',
  241. label: selectInvert,
  242. onClick() {
  243. var _a;
  244. const availablePageItemKeys = getEnabledItemKeys((((_a = listBodyRef.current) === null || _a === void 0 ? void 0 : _a.items) || []).map(entity => entity.item));
  245. const checkedKeySet = new Set(checkedKeys);
  246. const newCheckedKeysSet = new Set(checkedKeySet);
  247. availablePageItemKeys.forEach(key => {
  248. if (checkedKeySet.has(key)) {
  249. newCheckedKeysSet.delete(key);
  250. } else {
  251. newCheckedKeysSet.add(key);
  252. }
  253. });
  254. onItemSelectAll === null || onItemSelectAll === void 0 ? void 0 : onItemSelectAll(Array.from(newCheckedKeysSet), 'replace');
  255. }
  256. }];
  257. }
  258. const dropdown = /*#__PURE__*/React.createElement(Dropdown, {
  259. className: `${prefixCls}-header-dropdown`,
  260. menu: {
  261. items
  262. },
  263. disabled: disabled
  264. }, isValidIcon(selectionsIcon) ? selectionsIcon : /*#__PURE__*/React.createElement(DownOutlined, null));
  265. return /*#__PURE__*/React.createElement("div", {
  266. className: listCls,
  267. style: style
  268. }, /*#__PURE__*/React.createElement("div", {
  269. className: `${prefixCls}-header`
  270. }, showSelectAll ? (/*#__PURE__*/React.createElement(React.Fragment, null, checkAllCheckbox, dropdown)) : null, /*#__PURE__*/React.createElement("span", {
  271. className: `${prefixCls}-header-selected`
  272. }, getSelectAllLabel(checkedActiveItems.length, filteredItems.length)), /*#__PURE__*/React.createElement("span", {
  273. className: `${prefixCls}-header-title`
  274. }, titleText)), listBody, listFooter);
  275. };
  276. if (process.env.NODE_ENV !== 'production') {
  277. TransferList.displayName = 'TransferList';
  278. }
  279. export default TransferList;