ant-design/components/table/hooks/useSelection.tsx
陈帅 523b74e3b6
merge master into Feature (#25262)
* feat: add successColor for Progress (#24655)

* feat: add successColor for Progress

* feat: update

* fix: update test

* remove snap

* feat: add test case

* refactor success

* feat: adjust styyle

* feat: add DevWarning

* feat: Support rowSelection.dirty (#24718)

* feat: Support rowSelection.dirty

* rename to reserveKeys

* preserveKeys will keep record also

* to preserveSelectedRowKeys

* feat: add ghost prop for collapse (#24734)

* feat: add ghost prop for collapse

* doc: version of collapse's ghost prop

* refactor: make ghost collapse's less code to a nested style

* chore: remove redundant codes in ghost collapse's less & doc

* doc: add a background wrapper for ghost collapse demo

* doc: dark-theme wrapper bg-color for ghost collapse demo

* test: update snapshot of ghost collapse

* doc: use softer bg-color on ghost collapse demo

* doc: remove disabled panel in ghost collapse demo

* feat: form instance support getFieldInstance (#24711)

* support getFieldInstance

* update doc

* fix lint

* move func

* move into hooks

* update ref logic

* fix lint

* rm only

* fix docs

* feat: dropdown support arrow (#23869)

* feat: dropdown support arrow prop

close #22758

* test: update snapshot

* fix: fix dropdown cls names

* test: update snapshot

* test: update snapshot

* doc: update demo

* test: update demo snapshot

* demo

* fix: snapshot

* chore: change the style of ghost collapse & demo modified (#24762)

* refactor: reduce content padding in ghost collapse

* doc: remove the wrapper outside ghost collapse

Designer want the demo differs from other demos

* refactor: remove redundant .less code in collapse

* feat: cascader dropdown-render prop (#24812)

* feat: cascader dropdown-render prop

* fix: update Cascader dropdownRender type annotation

* fix: set rc-cascader semver from ^ to ~

* docs: fix coding style in cascader/custom-dropdown

* feat: 🆕 support Drawer closeIcon (#24842)

* feat: 🆕 support Drawer closeIcon

close #19283
close #19153

* add test case

* update docs

* feat: 🆕 Cascader expandIcon (#24865)

* feat: cascader expandIcon

* fix: snap

* refactor: reduce CSS size (#24846)

* refactor: reduce button css size

* refactor: remove redundant button .less code

* feat: add Table onChange an action param (#24697)

* Working on tests

* created TableAction type

* changed TableActions to tuple

* removed chinese documentation line

* refactor TableActions

* fix documentation

* Moved action into extra param

* minor doc change

* feat: add closeIcon customize tag close (#24885)

* feat: add closeIcon customize tag close

* docs fix

* update snap

* fix: css name

* update snapshot

* snapshot

* feat: add radio `optionType` api to set radio option type (#24809)

* feat: radio component

* docs: update md

* fix: snap

* test components

* fix: use optionType

* fix name

* add warning

* fix

* feat: expand rate character (#24903)

* feat: expand rate character

* fix: demo

* fix: snap

* Update components/rate/index.zh-CN.md

Co-authored-by: 偏右 <afc163@gmail.com>

* fix

Co-authored-by: 偏右 <afc163@gmail.com>

* Refactor demo code box actions (#24887)

* refactor: refine the styling of actions part of demo code-box

* fix: lint style

* refactor: move Result children to end (#24945)

* feat: remove content max-width on dot-step (#24907)

* feat: add Skeleton-Image (#24805)

* feat: add Skeleton-Image

* feat: add docs

* fix: adjust skeleton

* feat: adjust Image Component

* feat: rebase

* feat: adjust style

* fix: lint

* feat: remove size

* feat: delete md

* feat: fix style

*  feat: Mentions support autoSize (#24961)

close #17746

* chore: replace textarea with rc-textarea (#24966)

* feat: update pagination@2.3.0 support onChange called when pageSize change (#24964)

* feat: update pagination@2.5.0 and add test case to relative component

* fix: lint

* delete

* feat: add test case for pagination

* adjust test case

* feat: Implement centered prop in Tabs (#24958)

* Implement centered in Tabs along with its tests and docs

* Fix build error

* Add Chinese translations and remove test case

Co-authored-by: Ashkan Pourghasem <ashkan.pourghasem@gmail.com>

* feat: Add modal style parameter (#24773)

* add some paramters in default.less

* Update components/style/themes/default.less

Co-authored-by: Amumu <yoyo837@hotmail.com>

* change parameter in compact.less

Co-authored-by: Crystal Gao <jinggao@ebay.com>
Co-authored-by: Amumu <yoyo837@hotmail.com>

* feat: export Tabs addIcon (#25006)

* feat: export Tabs addIcon

* update snapshot

* feat: showNow on timepicker and datetimepicker (#25032)

* feat: update rc-picker@1.7.1 and fix icons of month and quarter picker in DatePicker Component (#25035)

* feat: update rc-picker@1.7.1

* delete

* add

* feat: expand rate support props (#24993)

* docs: 📝 Add Form.Item hidden in doc (#25108)

close #25101

* fix: ⌨️ Improve Pagination accessibility issue (#25119)

* ⌨️ Improve Pagination a11y by fixing a W3C error

https://github.com/react-component/pagination/issues/280

* update snapshot

* 🆙 rc-pagination to 2.4.1

* feat: support triggerSubMenuAction for <Menu /> (#25127)

* feat(menu): add triggerSubMenuAction for Menu

* feat(menu): test cases

* chore: Adjust picker logic (#25135)

* chore: update rc-picker 1.10.0 (#25174)

* feat: table row check strictly (#24931)

* feat: add checkStrictly on Table.rowSelection

* fix: LGTM warnings

* test: table rowSelection.checkStrictly

* test: add cov [wip]

* refactor: tree.rowSelection.checkStrictly [wip]

* test: table.rowSelection.checkStrictly basic case

* feat: support rowKey on checkStrictly table

* feat: Table checkStrictly support getCheckboxProps

* docs: Table checkStrictly

* chore: typo

* chore: remove useless comment

* chore: update snapshot

* chore: update snapshot

* fix: fire selectAll on selection dropdown menu & changeRows incorrect in selectAll callback

* docs: typo

* chore

* chore

* fix: expand buttons of leaf rows in tree data are not hidden

* feat: Table warning about rowKey index parameter

* perf: only generate keyEntities when not checkStrictly

* refactor: remove useless parseCheckedKeys

* refactor: get derived selected & half selected keys from selectedRowKeys

* chore: remove env condition stmt

* chore: revert index usage & code formatting

* chore: rerun ci

* docs: table tree-data checkstrictly

* test: update snapshots

* refactor: use useMergedState hook

* chore: rerun ci

* chore: rerun ci 2

* chore: revert selection select all behavior

* refactor: refactor code based on feature

* chore: revert table code format

* chore: revert table code format

* fix: useMemo deps

* fix: useMemo deps

* fix: useMemo deps

* feat: support preserve (#25186)

* docs: add responsibly order for Col (#25139)

* feat: add type

* feat: add responsibly order cols

* feat: add docs

* feat: add test case

* fix test

Co-authored-by: 二货机器人 <smith3816@gmail.com>
Co-authored-by: 偏右 <afc163@gmail.com>
Co-authored-by: zoomdong <1344492820@qq.com>
Co-authored-by: 07akioni <07akioni2@gmail.com>
Co-authored-by: wendellhu <wendellhu95@gmail.com>
Co-authored-by: xrkffgg <xrkffgg@gmail.com>
Co-authored-by: Neto Braghetto <netow93@gmail.com>
Co-authored-by: Kermit Xuan <kermitlx@outlook.com>
Co-authored-by: Ashkan Pourghasem <64011067+ashkan-pm@users.noreply.github.com>
Co-authored-by: Ashkan Pourghasem <ashkan.pourghasem@gmail.com>
Co-authored-by: hicrystal <295247343@qq.com>
Co-authored-by: Crystal Gao <jinggao@ebay.com>
Co-authored-by: Amumu <yoyo837@hotmail.com>
Co-authored-by: Li Ming <armyiljfe@gmail.com>
2020-06-28 22:41:59 +08:00

592 lines
18 KiB
TypeScript

import * as React from 'react';
import { useState, useCallback, useMemo } from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined';
import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil';
import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
import { arrAdd, arrDel } from 'rc-tree/lib/util';
import { DataNode, GetCheckDisabled } from 'rc-tree/lib/interface';
import { INTERNAL_COL_DEFINE } from 'rc-table';
import { FixedType } from 'rc-table/lib/interface';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Checkbox, { CheckboxProps } from '../../checkbox';
import Dropdown from '../../dropdown';
import Menu from '../../menu';
import Radio from '../../radio';
import devWarning from '../../_util/devWarning';
import {
TableRowSelection,
Key,
ColumnsType,
GetRowKey,
TableLocale,
SelectionItem,
TransformColumns,
ExpandType,
GetPopupContainer,
} from '../interface';
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
function getFixedType<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
return column && column.fixed;
}
interface UseSelectionConfig<RecordType> {
prefixCls: string;
pageData: RecordType[];
data: RecordType[];
getRowKey: GetRowKey<RecordType>;
getRecordByKey: (key: Key) => RecordType;
expandType: ExpandType;
childrenColumnName: string;
expandIconColumnIndex?: number;
locale: TableLocale;
getPopupContainer?: GetPopupContainer;
}
export type INTERNAL_SELECTION_ITEM =
| SelectionItem
| typeof SELECTION_ALL
| typeof SELECTION_INVERT;
function flattenData<RecordType>(
data: RecordType[] | undefined,
childrenColumnName: string,
): RecordType[] {
let list: RecordType[] = [];
(data || []).forEach(record => {
list.push(record);
if (childrenColumnName in record) {
list = [
...list,
...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName),
];
}
});
return list;
}
export default function useSelection<RecordType>(
rowSelection: TableRowSelection<RecordType> | undefined,
config: UseSelectionConfig<RecordType>,
): [TransformColumns<RecordType>, Set<Key>] {
const {
preserveSelectedRowKeys,
selectedRowKeys,
getCheckboxProps,
onChange: onSelectionChange,
onSelect,
onSelectAll,
onSelectInvert,
onSelectMultiple,
columnWidth: selectionColWidth,
type: selectionType,
selections,
fixed,
renderCell: customizeRenderCell,
hideSelectAll,
checkStrictly = true,
} = rowSelection || {};
const {
prefixCls,
data,
pageData,
getRecordByKey,
getRowKey,
expandType,
childrenColumnName,
locale: tableLocale,
expandIconColumnIndex,
getPopupContainer,
} = config;
// ======================== Caches ========================
const preserveRecordsRef = React.useRef(new Map<Key, RecordType>());
// ========================= Keys =========================
const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(selectedRowKeys || [], {
value: selectedRowKeys,
});
const { keyEntities } = useMemo(
() =>
checkStrictly
? { keyEntities: null }
: convertDataToEntities((data as unknown) as DataNode[], undefined, getRowKey as any),
[data, getRowKey, checkStrictly],
);
// Get flatten data
const flattedData = useMemo(() => flattenData(pageData, childrenColumnName), [
pageData,
childrenColumnName,
]);
// Get all checkbox props
const checkboxPropsMap = useMemo(() => {
const map = new Map<Key, Partial<CheckboxProps>>();
flattedData.forEach((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
map.set(key, checkboxProps);
if (
process.env.NODE_ENV !== 'production' &&
('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
) {
devWarning(
false,
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
});
return map;
}, [flattedData, getRowKey, getCheckboxProps]);
const isCheckboxDisabled: GetCheckDisabled<RecordType> = useCallback(
(r: RecordType) => {
return !!checkboxPropsMap.get(getRowKey(r))?.disabled;
},
[checkboxPropsMap, getRowKey],
);
const [derivedSelectedKeys, derivedHalfSelectedKeys] = useMemo(() => {
if (checkStrictly) {
return [mergedSelectedKeys, []];
}
const { checkedKeys, halfCheckedKeys } = conductCheck(
mergedSelectedKeys,
true,
keyEntities as any,
isCheckboxDisabled as any,
);
return [checkedKeys, halfCheckedKeys];
}, [mergedSelectedKeys, checkStrictly, keyEntities, isCheckboxDisabled]);
const derivedSelectedKeySet: Set<Key> = useMemo(() => {
const keys = selectionType === 'radio' ? derivedSelectedKeys.slice(0, 1) : derivedSelectedKeys;
return new Set(keys);
}, [derivedSelectedKeys, selectionType]);
const derivedHalfSelectedKeySet = useMemo(() => {
return selectionType === 'radio' ? new Set() : new Set(derivedHalfSelectedKeys);
}, [derivedHalfSelectedKeys, selectionType]);
// Save last selected key to enable range selection
const [lastSelectedKey, setLastSelectedKey] = useState<Key | null>(null);
// Reset if rowSelection reset
React.useEffect(() => {
if (!rowSelection) {
setMergedSelectedKeys([]);
}
}, [!!rowSelection]);
const setSelectedKeys = useCallback(
(keys: Key[]) => {
let availableKeys: Key[];
let records: RecordType[];
if (preserveSelectedRowKeys) {
// Keep key if mark as preserveSelectedRowKeys
const newCache = new Map<Key, RecordType>();
availableKeys = keys;
records = keys.map(key => {
let record = getRecordByKey(key);
if (!record && preserveRecordsRef.current.has(key)) {
record = preserveRecordsRef.current.get(key)!;
}
newCache.set(key, record);
return record;
});
// Refresh to new cache
preserveRecordsRef.current = newCache;
} else {
// Filter key which not exist in the `dataSource`
availableKeys = [];
records = [];
keys.forEach(key => {
const record = getRecordByKey(key);
if (record !== undefined) {
availableKeys.push(key);
records.push(record);
}
});
}
setMergedSelectedKeys(availableKeys);
if (onSelectionChange) {
onSelectionChange(availableKeys, records);
}
},
[setMergedSelectedKeys, getRecordByKey, onSelectionChange, preserveSelectedRowKeys],
);
// ====================== Selections ======================
// Trigger single `onSelect` event
const triggerSingleSelection = useCallback(
(key: Key, selected: boolean, keys: Key[], event: Event) => {
if (onSelect) {
const rows = keys.map(k => getRecordByKey(k));
onSelect(getRecordByKey(key), selected, rows, event);
}
setSelectedKeys(keys);
},
[onSelect, getRecordByKey, setSelectedKeys],
);
const mergedSelections = useMemo<SelectionItem[] | null>(() => {
if (!selections || hideSelectAll) {
return null;
}
const selectionList: INTERNAL_SELECTION_ITEM[] =
selections === true ? [SELECTION_ALL, SELECTION_INVERT] : selections;
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => {
if (selection === SELECTION_ALL) {
return {
key: 'all',
text: tableLocale.selectionAll,
onSelect() {
setSelectedKeys(data.map((record, index) => getRowKey(record, index)));
},
};
}
if (selection === SELECTION_INVERT) {
return {
key: 'invert',
text: tableLocale.selectInvert,
onSelect() {
const keySet = new Set(derivedSelectedKeySet);
pageData.forEach((record, index) => {
const key = getRowKey(record, index);
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
}
});
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectInvert) {
devWarning(
false,
'Table',
'`onSelectInvert` will be removed in future. Please use `onChange` instead.',
);
onSelectInvert(keys);
}
},
};
}
return selection as SelectionItem;
});
}, [selections, derivedSelectedKeySet, pageData, getRowKey, onSelectInvert, setSelectedKeys]);
// ======================= Columns ========================
const transformColumns = useCallback(
(columns: ColumnsType<RecordType>): ColumnsType<RecordType> => {
if (!rowSelection) {
return columns;
}
// Support selection
const keySet = new Set(derivedSelectedKeySet);
// Record key only need check with enabled
const recordKeys = flattedData
.map(getRowKey)
.filter(key => !checkboxPropsMap.get(key)!.disabled);
const checkedCurrentAll = recordKeys.every(key => keySet.has(key));
const checkedCurrentSome = recordKeys.some(key => keySet.has(key));
const onSelectAllChange = () => {
const changeKeys: Key[] = [];
if (checkedCurrentAll) {
recordKeys.forEach(key => {
keySet.delete(key);
changeKeys.push(key);
});
} else {
recordKeys.forEach(key => {
if (!keySet.has(key)) {
keySet.add(key);
changeKeys.push(key);
}
});
}
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectAll) {
onSelectAll(
!checkedCurrentAll,
keys.map(k => getRecordByKey(k)),
changeKeys.map(k => getRecordByKey(k)),
);
}
};
// ===================== Render =====================
// Title Cell
let title: React.ReactNode;
if (selectionType !== 'radio') {
let customizeSelections: React.ReactNode;
if (mergedSelections) {
const menu = (
<Menu getPopupContainer={getPopupContainer}>
{mergedSelections.map((selection, index) => {
const { key, text, onSelect: onSelectionClick } = selection;
return (
<Menu.Item
key={key || index}
onClick={() => {
if (onSelectionClick) {
onSelectionClick(recordKeys);
}
}}
>
{text}
</Menu.Item>
);
})}
</Menu>
);
customizeSelections = (
<div className={`${prefixCls}-selection-extra`}>
<Dropdown overlay={menu} getPopupContainer={getPopupContainer}>
<span>
<DownOutlined />
</span>
</Dropdown>
</div>
);
}
const allDisabled = flattedData.every((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = checkboxPropsMap.get(key) || {};
return checkboxProps.disabled;
});
title = !hideSelectAll && (
<div className={`${prefixCls}-selection`}>
<Checkbox
checked={!allDisabled && !!flattedData.length && checkedCurrentAll}
indeterminate={!checkedCurrentAll && checkedCurrentSome}
onChange={onSelectAllChange}
disabled={flattedData.length === 0 || allDisabled}
/>
{customizeSelections}
</div>
);
}
// Body Cell
let renderCell: (
_: RecordType,
record: RecordType,
index: number,
) => { node: React.ReactNode; checked: boolean };
if (selectionType === 'radio') {
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const checked = keySet.has(key);
return {
node: (
<Radio
{...checkboxPropsMap.get(key)}
checked={checked}
onClick={e => e.stopPropagation()}
onChange={event => {
if (!keySet.has(key)) {
triggerSingleSelection(key, true, [key], event.nativeEvent);
}
}}
/>
),
checked,
};
};
} else {
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const checked = keySet.has(key);
const indeterminate = derivedHalfSelectedKeySet.has(key);
// Record checked
return {
node: (
<Checkbox
{...checkboxPropsMap.get(key)}
checked={checked}
indeterminate={indeterminate}
onClick={e => e.stopPropagation()}
onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent;
let startIndex: number = -1;
let endIndex: number = -1;
// Get range of this
if (shiftKey && checkStrictly) {
const pointKeys = new Set([lastSelectedKey, key]);
recordKeys.some((recordKey, recordIndex) => {
if (pointKeys.has(recordKey)) {
if (startIndex === -1) {
startIndex = recordIndex;
} else {
endIndex = recordIndex;
return true;
}
}
return false;
});
}
if (endIndex !== -1 && startIndex !== endIndex && checkStrictly) {
// Batch update selections
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = [];
if (checked) {
rangeKeys.forEach(recordKey => {
if (keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.delete(recordKey);
}
});
} else {
rangeKeys.forEach(recordKey => {
if (!keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.add(recordKey);
}
});
}
const keys = Array.from(keySet);
setSelectedKeys(keys);
if (onSelectMultiple) {
onSelectMultiple(
!checked,
keys.map(recordKey => getRecordByKey(recordKey)),
changedKeys.map(recordKey => getRecordByKey(recordKey)),
);
}
} else {
// Single record selected
const originCheckedKeys = derivedSelectedKeys;
if (checkStrictly) {
const checkedKeys = checked
? arrDel(originCheckedKeys, key)
: arrAdd(originCheckedKeys, key);
triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
} else {
// Always fill first
const result = conductCheck(
[...originCheckedKeys, key],
true,
keyEntities as any,
isCheckboxDisabled as any,
);
const { checkedKeys, halfCheckedKeys } = result;
let nextCheckedKeys = checkedKeys;
// If remove, we do it again to correction
if (checked) {
const tempKeySet = new Set(checkedKeys);
tempKeySet.delete(key);
nextCheckedKeys = conductCheck(
Array.from(tempKeySet),
{ checked: false, halfCheckedKeys },
keyEntities as any,
isCheckboxDisabled as any,
).checkedKeys;
}
triggerSingleSelection(key, !checked, nextCheckedKeys, nativeEvent);
}
}
setLastSelectedKey(key);
}}
/>
),
checked,
};
};
}
const renderSelectionCell = (_: any, record: RecordType, index: number) => {
const { node, checked } = renderCell(_, record, index);
if (customizeRenderCell) {
return customizeRenderCell(checked, record, index, node);
}
return node;
};
// Columns
const selectionColumn = {
width: selectionColWidth,
className: `${prefixCls}-selection-column`,
title: rowSelection.columnTitle || title,
render: renderSelectionCell,
[INTERNAL_COL_DEFINE]: {
className: `${prefixCls}-selection-col`,
},
};
if (expandType === 'row' && columns.length && !expandIconColumnIndex) {
const [expandColumn, ...restColumns] = columns;
const selectionFixed = fixed || getFixedType(restColumns[0]);
if (selectionFixed) {
expandColumn.fixed = selectionFixed;
}
return [expandColumn, { ...selectionColumn, fixed: selectionFixed }, ...restColumns];
}
return [{ ...selectionColumn, fixed: fixed || getFixedType(columns[0]) }, ...columns];
},
[
getRowKey,
flattedData,
rowSelection,
derivedSelectedKeys,
derivedSelectedKeySet,
derivedHalfSelectedKeySet,
selectionColWidth,
mergedSelections,
expandType,
lastSelectedKey,
checkboxPropsMap,
onSelectMultiple,
triggerSingleSelection,
isCheckboxDisabled,
],
);
return [transformColumns, derivedSelectedKeySet];
}