useSelection.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { createVNode as _createVNode } from "vue";
  4. import DownOutlined from "@ant-design/icons-vue/es/icons/DownOutlined";
  5. import { INTERNAL_COL_DEFINE } from '../../vc-table';
  6. import { arrAdd, arrDel } from '../../vc-tree/util';
  7. import { conductCheck } from '../../vc-tree/utils/conductUtil';
  8. import { convertDataToEntities } from '../../vc-tree/utils/treeUtil';
  9. import devWarning from '../../vc-util/devWarning';
  10. import useMergedState from '../../_util/hooks/useMergedState';
  11. import useState from '../../_util/hooks/useState';
  12. import { watchEffect, computed, shallowRef } from 'vue';
  13. import Checkbox from '../../checkbox';
  14. import Dropdown from '../../dropdown';
  15. import Menu from '../../menu';
  16. import Radio from '../../radio';
  17. import useMaxLevel from '../../vc-tree/useMaxLevel';
  18. // TODO: warning if use ajax!!!
  19. export const SELECTION_COLUMN = {};
  20. export const SELECTION_ALL = 'SELECT_ALL';
  21. export const SELECTION_INVERT = 'SELECT_INVERT';
  22. export const SELECTION_NONE = 'SELECT_NONE';
  23. const EMPTY_LIST = [];
  24. function flattenData(childrenColumnName, data) {
  25. let list = [];
  26. (data || []).forEach(record => {
  27. list.push(record);
  28. if (record && typeof record === 'object' && childrenColumnName in record) {
  29. list = [...list, ...flattenData(childrenColumnName, record[childrenColumnName])];
  30. }
  31. });
  32. return list;
  33. }
  34. export default function useSelection(rowSelectionRef, configRef) {
  35. const mergedRowSelection = computed(() => {
  36. const temp = rowSelectionRef.value || {};
  37. const {
  38. checkStrictly = true
  39. } = temp;
  40. return _extends(_extends({}, temp), {
  41. checkStrictly
  42. });
  43. });
  44. // ========================= Keys =========================
  45. const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(mergedRowSelection.value.selectedRowKeys || mergedRowSelection.value.defaultSelectedRowKeys || EMPTY_LIST, {
  46. value: computed(() => mergedRowSelection.value.selectedRowKeys)
  47. });
  48. // ======================== Caches ========================
  49. const preserveRecordsRef = shallowRef(new Map());
  50. const updatePreserveRecordsCache = keys => {
  51. if (mergedRowSelection.value.preserveSelectedRowKeys) {
  52. const newCache = new Map();
  53. // Keep key if mark as preserveSelectedRowKeys
  54. keys.forEach(key => {
  55. let record = configRef.getRecordByKey(key);
  56. if (!record && preserveRecordsRef.value.has(key)) {
  57. record = preserveRecordsRef.value.get(key);
  58. }
  59. newCache.set(key, record);
  60. });
  61. // Refresh to new cache
  62. preserveRecordsRef.value = newCache;
  63. }
  64. };
  65. watchEffect(() => {
  66. updatePreserveRecordsCache(mergedSelectedKeys.value);
  67. });
  68. const keyEntities = computed(() => mergedRowSelection.value.checkStrictly ? null : convertDataToEntities(configRef.data.value, {
  69. externalGetKey: configRef.getRowKey.value,
  70. childrenPropName: configRef.childrenColumnName.value
  71. }).keyEntities);
  72. // Get flatten data
  73. const flattedData = computed(() => flattenData(configRef.childrenColumnName.value, configRef.pageData.value));
  74. // Get all checkbox props
  75. const checkboxPropsMap = computed(() => {
  76. const map = new Map();
  77. const getRowKey = configRef.getRowKey.value;
  78. const getCheckboxProps = mergedRowSelection.value.getCheckboxProps;
  79. flattedData.value.forEach((record, index) => {
  80. const key = getRowKey(record, index);
  81. const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
  82. map.set(key, checkboxProps);
  83. if (process.env.NODE_ENV !== 'production' && ('checked' in checkboxProps || 'defaultChecked' in checkboxProps)) {
  84. devWarning(false, 'Table', 'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.');
  85. }
  86. });
  87. return map;
  88. });
  89. const {
  90. maxLevel,
  91. levelEntities
  92. } = useMaxLevel(keyEntities);
  93. const isCheckboxDisabled = r => {
  94. var _a;
  95. return !!((_a = checkboxPropsMap.value.get(configRef.getRowKey.value(r))) === null || _a === void 0 ? void 0 : _a.disabled);
  96. };
  97. const selectKeysState = computed(() => {
  98. if (mergedRowSelection.value.checkStrictly) {
  99. return [mergedSelectedKeys.value || [], []];
  100. }
  101. const {
  102. checkedKeys,
  103. halfCheckedKeys
  104. } = conductCheck(mergedSelectedKeys.value, true, keyEntities.value, maxLevel.value, levelEntities.value, isCheckboxDisabled);
  105. return [checkedKeys || [], halfCheckedKeys];
  106. });
  107. const derivedSelectedKeys = computed(() => selectKeysState.value[0]);
  108. const derivedHalfSelectedKeys = computed(() => selectKeysState.value[1]);
  109. const derivedSelectedKeySet = computed(() => {
  110. const keys = mergedRowSelection.value.type === 'radio' ? derivedSelectedKeys.value.slice(0, 1) : derivedSelectedKeys.value;
  111. return new Set(keys);
  112. });
  113. const derivedHalfSelectedKeySet = computed(() => mergedRowSelection.value.type === 'radio' ? new Set() : new Set(derivedHalfSelectedKeys.value));
  114. // Save last selected key to enable range selection
  115. const [lastSelectedKey, setLastSelectedKey] = useState(null);
  116. // // Reset if rowSelection reset
  117. // we use computed to reset, donot need setMergedSelectedKeys again like react
  118. // https://github.com/vueComponent/ant-design-vue/issues/4885
  119. // watchEffect(() => {
  120. // if (!rowSelectionRef.value) {
  121. // setMergedSelectedKeys([]);
  122. // }
  123. // });
  124. const setSelectedKeys = keys => {
  125. let availableKeys;
  126. let records;
  127. updatePreserveRecordsCache(keys);
  128. const {
  129. preserveSelectedRowKeys,
  130. onChange: onSelectionChange
  131. } = mergedRowSelection.value;
  132. const {
  133. getRecordByKey
  134. } = configRef;
  135. if (preserveSelectedRowKeys) {
  136. availableKeys = keys;
  137. records = keys.map(key => preserveRecordsRef.value.get(key));
  138. } else {
  139. // Filter key which not exist in the `dataSource`
  140. availableKeys = [];
  141. records = [];
  142. keys.forEach(key => {
  143. const record = getRecordByKey(key);
  144. if (record !== undefined) {
  145. availableKeys.push(key);
  146. records.push(record);
  147. }
  148. });
  149. }
  150. setMergedSelectedKeys(availableKeys);
  151. onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange(availableKeys, records);
  152. };
  153. // ====================== Selections ======================
  154. // Trigger single `onSelect` event
  155. const triggerSingleSelection = (key, selected, keys, event) => {
  156. const {
  157. onSelect
  158. } = mergedRowSelection.value;
  159. const {
  160. getRecordByKey
  161. } = configRef || {};
  162. if (onSelect) {
  163. const rows = keys.map(k => getRecordByKey(k));
  164. onSelect(getRecordByKey(key), selected, rows, event);
  165. }
  166. setSelectedKeys(keys);
  167. };
  168. const mergedSelections = computed(() => {
  169. const {
  170. onSelectInvert,
  171. onSelectNone,
  172. selections,
  173. hideSelectAll
  174. } = mergedRowSelection.value;
  175. const {
  176. data,
  177. pageData,
  178. getRowKey,
  179. locale: tableLocale
  180. } = configRef;
  181. if (!selections || hideSelectAll) {
  182. return null;
  183. }
  184. const selectionList = selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections;
  185. return selectionList.map(selection => {
  186. if (selection === SELECTION_ALL) {
  187. return {
  188. key: 'all',
  189. text: tableLocale.value.selectionAll,
  190. onSelect() {
  191. setSelectedKeys(data.value.map((record, index) => getRowKey.value(record, index)).filter(key => {
  192. const checkProps = checkboxPropsMap.value.get(key);
  193. return !(checkProps === null || checkProps === void 0 ? void 0 : checkProps.disabled) || derivedSelectedKeySet.value.has(key);
  194. }));
  195. }
  196. };
  197. }
  198. if (selection === SELECTION_INVERT) {
  199. return {
  200. key: 'invert',
  201. text: tableLocale.value.selectInvert,
  202. onSelect() {
  203. const keySet = new Set(derivedSelectedKeySet.value);
  204. pageData.value.forEach((record, index) => {
  205. const key = getRowKey.value(record, index);
  206. const checkProps = checkboxPropsMap.value.get(key);
  207. if (!(checkProps === null || checkProps === void 0 ? void 0 : checkProps.disabled)) {
  208. if (keySet.has(key)) {
  209. keySet.delete(key);
  210. } else {
  211. keySet.add(key);
  212. }
  213. }
  214. });
  215. const keys = Array.from(keySet);
  216. if (onSelectInvert) {
  217. devWarning(false, 'Table', '`onSelectInvert` will be removed in future. Please use `onChange` instead.');
  218. onSelectInvert(keys);
  219. }
  220. setSelectedKeys(keys);
  221. }
  222. };
  223. }
  224. if (selection === SELECTION_NONE) {
  225. return {
  226. key: 'none',
  227. text: tableLocale.value.selectNone,
  228. onSelect() {
  229. onSelectNone === null || onSelectNone === void 0 ? void 0 : onSelectNone();
  230. setSelectedKeys(Array.from(derivedSelectedKeySet.value).filter(key => {
  231. const checkProps = checkboxPropsMap.value.get(key);
  232. return checkProps === null || checkProps === void 0 ? void 0 : checkProps.disabled;
  233. }));
  234. }
  235. };
  236. }
  237. return selection;
  238. });
  239. });
  240. const flattedDataLength = computed(() => flattedData.value.length);
  241. // ======================= Columns ========================
  242. const transformColumns = columns => {
  243. var _a;
  244. const {
  245. onSelectAll,
  246. onSelectMultiple,
  247. columnWidth: selectionColWidth,
  248. type: selectionType,
  249. fixed,
  250. renderCell: customizeRenderCell,
  251. hideSelectAll,
  252. checkStrictly
  253. } = mergedRowSelection.value;
  254. const {
  255. prefixCls,
  256. getRecordByKey,
  257. getRowKey,
  258. expandType,
  259. getPopupContainer
  260. } = configRef;
  261. if (!rowSelectionRef.value) {
  262. if (process.env.NODE_ENV !== 'production') {
  263. devWarning(!columns.includes(SELECTION_COLUMN), 'Table', '`rowSelection` is not config but `SELECTION_COLUMN` exists in the `columns`.');
  264. }
  265. return columns.filter(col => col !== SELECTION_COLUMN);
  266. }
  267. // Support selection
  268. let cloneColumns = columns.slice();
  269. const keySet = new Set(derivedSelectedKeySet.value);
  270. // Record key only need check with enabled
  271. const recordKeys = flattedData.value.map(getRowKey.value).filter(key => !checkboxPropsMap.value.get(key).disabled);
  272. const checkedCurrentAll = recordKeys.every(key => keySet.has(key));
  273. const checkedCurrentSome = recordKeys.some(key => keySet.has(key));
  274. const onSelectAllChange = () => {
  275. const changeKeys = [];
  276. if (checkedCurrentAll) {
  277. recordKeys.forEach(key => {
  278. keySet.delete(key);
  279. changeKeys.push(key);
  280. });
  281. } else {
  282. recordKeys.forEach(key => {
  283. if (!keySet.has(key)) {
  284. keySet.add(key);
  285. changeKeys.push(key);
  286. }
  287. });
  288. }
  289. const keys = Array.from(keySet);
  290. onSelectAll === null || onSelectAll === void 0 ? void 0 : onSelectAll(!checkedCurrentAll, keys.map(k => getRecordByKey(k)), changeKeys.map(k => getRecordByKey(k)));
  291. setSelectedKeys(keys);
  292. };
  293. // ===================== Render =====================
  294. // Title Cell
  295. let title;
  296. if (selectionType !== 'radio') {
  297. let customizeSelections;
  298. if (mergedSelections.value) {
  299. const menu = _createVNode(Menu, {
  300. "getPopupContainer": getPopupContainer.value
  301. }, {
  302. default: () => [mergedSelections.value.map((selection, index) => {
  303. const {
  304. key,
  305. text,
  306. onSelect: onSelectionClick
  307. } = selection;
  308. return _createVNode(Menu.Item, {
  309. "key": key || index,
  310. "onClick": () => {
  311. onSelectionClick === null || onSelectionClick === void 0 ? void 0 : onSelectionClick(recordKeys);
  312. }
  313. }, {
  314. default: () => [text]
  315. });
  316. })]
  317. });
  318. customizeSelections = _createVNode("div", {
  319. "class": `${prefixCls.value}-selection-extra`
  320. }, [_createVNode(Dropdown, {
  321. "overlay": menu,
  322. "getPopupContainer": getPopupContainer.value
  323. }, {
  324. default: () => [_createVNode("span", null, [_createVNode(DownOutlined, null, null)])]
  325. })]);
  326. }
  327. const allDisabledData = flattedData.value.map((record, index) => {
  328. const key = getRowKey.value(record, index);
  329. const checkboxProps = checkboxPropsMap.value.get(key) || {};
  330. return _extends({
  331. checked: keySet.has(key)
  332. }, checkboxProps);
  333. }).filter(_ref => {
  334. let {
  335. disabled
  336. } = _ref;
  337. return disabled;
  338. });
  339. const allDisabled = !!allDisabledData.length && allDisabledData.length === flattedDataLength.value;
  340. const allDisabledAndChecked = allDisabled && allDisabledData.every(_ref2 => {
  341. let {
  342. checked
  343. } = _ref2;
  344. return checked;
  345. });
  346. const allDisabledSomeChecked = allDisabled && allDisabledData.some(_ref3 => {
  347. let {
  348. checked
  349. } = _ref3;
  350. return checked;
  351. });
  352. title = !hideSelectAll && _createVNode("div", {
  353. "class": `${prefixCls.value}-selection`
  354. }, [_createVNode(Checkbox, {
  355. "checked": !allDisabled ? !!flattedDataLength.value && checkedCurrentAll : allDisabledAndChecked,
  356. "indeterminate": !allDisabled ? !checkedCurrentAll && checkedCurrentSome : !allDisabledAndChecked && allDisabledSomeChecked,
  357. "onChange": onSelectAllChange,
  358. "disabled": flattedDataLength.value === 0 || allDisabled,
  359. "aria-label": customizeSelections ? 'Custom selection' : 'Select all',
  360. "skipGroup": true
  361. }, null), customizeSelections]);
  362. }
  363. // Body Cell
  364. let renderCell;
  365. if (selectionType === 'radio') {
  366. renderCell = _ref4 => {
  367. let {
  368. record,
  369. index
  370. } = _ref4;
  371. const key = getRowKey.value(record, index);
  372. const checked = keySet.has(key);
  373. return {
  374. node: _createVNode(Radio, _objectSpread(_objectSpread({}, checkboxPropsMap.value.get(key)), {}, {
  375. "checked": checked,
  376. "onClick": e => e.stopPropagation(),
  377. "onChange": event => {
  378. if (!keySet.has(key)) {
  379. triggerSingleSelection(key, true, [key], event.nativeEvent);
  380. }
  381. }
  382. }), null),
  383. checked
  384. };
  385. };
  386. } else {
  387. renderCell = _ref5 => {
  388. let {
  389. record,
  390. index
  391. } = _ref5;
  392. var _a;
  393. const key = getRowKey.value(record, index);
  394. const checked = keySet.has(key);
  395. const indeterminate = derivedHalfSelectedKeySet.value.has(key);
  396. const checkboxProps = checkboxPropsMap.value.get(key);
  397. let mergedIndeterminate;
  398. if (expandType.value === 'nest') {
  399. mergedIndeterminate = indeterminate;
  400. devWarning(typeof (checkboxProps === null || checkboxProps === void 0 ? void 0 : checkboxProps.indeterminate) !== 'boolean', 'Table', 'set `indeterminate` using `rowSelection.getCheckboxProps` is not allowed with tree structured dataSource.');
  401. } else {
  402. mergedIndeterminate = (_a = checkboxProps === null || checkboxProps === void 0 ? void 0 : checkboxProps.indeterminate) !== null && _a !== void 0 ? _a : indeterminate;
  403. }
  404. // Record checked
  405. return {
  406. node: _createVNode(Checkbox, _objectSpread(_objectSpread({}, checkboxProps), {}, {
  407. "indeterminate": mergedIndeterminate,
  408. "checked": checked,
  409. "skipGroup": true,
  410. "onClick": e => e.stopPropagation(),
  411. "onChange": _ref6 => {
  412. let {
  413. nativeEvent
  414. } = _ref6;
  415. const {
  416. shiftKey
  417. } = nativeEvent;
  418. let startIndex = -1;
  419. let endIndex = -1;
  420. // Get range of this
  421. if (shiftKey && checkStrictly) {
  422. const pointKeys = new Set([lastSelectedKey.value, key]);
  423. recordKeys.some((recordKey, recordIndex) => {
  424. if (pointKeys.has(recordKey)) {
  425. if (startIndex === -1) {
  426. startIndex = recordIndex;
  427. } else {
  428. endIndex = recordIndex;
  429. return true;
  430. }
  431. }
  432. return false;
  433. });
  434. }
  435. if (endIndex !== -1 && startIndex !== endIndex && checkStrictly) {
  436. // Batch update selections
  437. const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
  438. const changedKeys = [];
  439. if (checked) {
  440. rangeKeys.forEach(recordKey => {
  441. if (keySet.has(recordKey)) {
  442. changedKeys.push(recordKey);
  443. keySet.delete(recordKey);
  444. }
  445. });
  446. } else {
  447. rangeKeys.forEach(recordKey => {
  448. if (!keySet.has(recordKey)) {
  449. changedKeys.push(recordKey);
  450. keySet.add(recordKey);
  451. }
  452. });
  453. }
  454. const keys = Array.from(keySet);
  455. onSelectMultiple === null || onSelectMultiple === void 0 ? void 0 : onSelectMultiple(!checked, keys.map(recordKey => getRecordByKey(recordKey)), changedKeys.map(recordKey => getRecordByKey(recordKey)));
  456. setSelectedKeys(keys);
  457. } else {
  458. // Single record selected
  459. const originCheckedKeys = derivedSelectedKeys.value;
  460. if (checkStrictly) {
  461. const checkedKeys = checked ? arrDel(originCheckedKeys, key) : arrAdd(originCheckedKeys, key);
  462. triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
  463. } else {
  464. // Always fill first
  465. const result = conductCheck([...originCheckedKeys, key], true, keyEntities.value, maxLevel.value, levelEntities.value, isCheckboxDisabled);
  466. const {
  467. checkedKeys,
  468. halfCheckedKeys
  469. } = result;
  470. let nextCheckedKeys = checkedKeys;
  471. // If remove, we do it again to correction
  472. if (checked) {
  473. const tempKeySet = new Set(checkedKeys);
  474. tempKeySet.delete(key);
  475. nextCheckedKeys = conductCheck(Array.from(tempKeySet), {
  476. checked: false,
  477. halfCheckedKeys
  478. }, keyEntities.value, maxLevel.value, levelEntities.value, isCheckboxDisabled).checkedKeys;
  479. }
  480. triggerSingleSelection(key, !checked, nextCheckedKeys, nativeEvent);
  481. }
  482. }
  483. setLastSelectedKey(key);
  484. }
  485. }), null),
  486. checked
  487. };
  488. };
  489. }
  490. const renderSelectionCell = _ref7 => {
  491. let {
  492. record,
  493. index
  494. } = _ref7;
  495. const {
  496. node,
  497. checked
  498. } = renderCell({
  499. record,
  500. index
  501. });
  502. if (customizeRenderCell) {
  503. return customizeRenderCell(checked, record, index, node);
  504. }
  505. return node;
  506. };
  507. // Insert selection column if not exist
  508. if (!cloneColumns.includes(SELECTION_COLUMN)) {
  509. // Always after expand icon
  510. if (cloneColumns.findIndex(col => {
  511. var _a;
  512. return ((_a = col[INTERNAL_COL_DEFINE]) === null || _a === void 0 ? void 0 : _a.columnType) === 'EXPAND_COLUMN';
  513. }) === 0) {
  514. const [expandColumn, ...restColumns] = cloneColumns;
  515. cloneColumns = [expandColumn, SELECTION_COLUMN, ...restColumns];
  516. } else {
  517. // Normal insert at first column
  518. cloneColumns = [SELECTION_COLUMN, ...cloneColumns];
  519. }
  520. }
  521. // Deduplicate selection column
  522. const selectionColumnIndex = cloneColumns.indexOf(SELECTION_COLUMN);
  523. if (process.env.NODE_ENV !== 'production' && cloneColumns.filter(col => col === SELECTION_COLUMN).length > 1) {
  524. devWarning(false, 'Table', 'Multiple `SELECTION_COLUMN` exist in `columns`.');
  525. }
  526. cloneColumns = cloneColumns.filter((column, index) => column !== SELECTION_COLUMN || index === selectionColumnIndex);
  527. // Fixed column logic
  528. const prevCol = cloneColumns[selectionColumnIndex - 1];
  529. const nextCol = cloneColumns[selectionColumnIndex + 1];
  530. let mergedFixed = fixed;
  531. if (mergedFixed === undefined) {
  532. if ((nextCol === null || nextCol === void 0 ? void 0 : nextCol.fixed) !== undefined) {
  533. mergedFixed = nextCol.fixed;
  534. } else if ((prevCol === null || prevCol === void 0 ? void 0 : prevCol.fixed) !== undefined) {
  535. mergedFixed = prevCol.fixed;
  536. }
  537. }
  538. if (mergedFixed && prevCol && ((_a = prevCol[INTERNAL_COL_DEFINE]) === null || _a === void 0 ? void 0 : _a.columnType) === 'EXPAND_COLUMN' && prevCol.fixed === undefined) {
  539. prevCol.fixed = mergedFixed;
  540. }
  541. // Replace with real selection column
  542. const selectionColumn = {
  543. fixed: mergedFixed,
  544. width: selectionColWidth,
  545. className: `${prefixCls.value}-selection-column`,
  546. title: mergedRowSelection.value.columnTitle || title,
  547. customRender: renderSelectionCell,
  548. [INTERNAL_COL_DEFINE]: {
  549. class: `${prefixCls.value}-selection-col`
  550. }
  551. };
  552. return cloneColumns.map(col => col === SELECTION_COLUMN ? selectionColumn : col);
  553. };
  554. return [transformColumns, derivedSelectedKeySet];
  555. }