import FilterFilled from '@ant-design/icons/FilterFilled'; import classNames from 'classnames'; import isEqual from 'lodash/isEqual'; import type { FieldDataNode } from 'rc-tree'; import * as React from 'react'; import type { FilterState } from '.'; import { flattenKeys } from '.'; import Button from '../../../button'; import type { CheckboxChangeEvent } from '../../../checkbox'; import Checkbox from '../../../checkbox'; import { ConfigContext } from '../../../config-provider/context'; import Dropdown from '../../../dropdown'; import Empty from '../../../empty'; import type { MenuProps } from '../../../menu'; import Menu from '../../../menu'; import { OverrideProvider } from '../../../menu/OverrideContext'; import Radio from '../../../radio'; import type { EventDataNode } from '../../../tree'; import Tree from '../../../tree'; import useSyncState from '../../../_util/hooks/useSyncState'; import type { ColumnFilterItem, ColumnType, FilterSearchType, GetPopupContainer, Key, TableLocale, } from '../../interface'; import FilterSearch from './FilterSearch'; import FilterDropdownMenuWrapper from './FilterWrapper'; type FilterTreeDataNode = FieldDataNode<{ title: React.ReactNode; key: React.Key }>; interface FilterRestProps { confirm?: Boolean; closeDropdown?: Boolean; } function hasSubMenu(filters: ColumnFilterItem[]) { return filters.some(({ children }) => children); } function searchValueMatched(searchValue: string, text: React.ReactNode) { if (typeof text === 'string' || typeof text === 'number') { return text?.toString().toLowerCase().includes(searchValue.trim().toLowerCase()); } return false; } function renderFilterItems({ filters, prefixCls, filteredKeys, filterMultiple, searchValue, filterSearch, }: { filters: ColumnFilterItem[]; prefixCls: string; filteredKeys: Key[]; filterMultiple: boolean; searchValue: string; filterSearch: FilterSearchType; }): Required['items'] { return filters.map((filter, index) => { const key = String(filter.value); if (filter.children) { return { key: key || index, label: filter.text, popupClassName: `${prefixCls}-dropdown-submenu`, children: renderFilterItems({ filters: filter.children, prefixCls, filteredKeys, filterMultiple, searchValue, filterSearch, }), }; } const Component = filterMultiple ? Checkbox : Radio; const item = { key: filter.value !== undefined ? key : index, label: ( <> {filter.text} ), }; if (searchValue.trim()) { if (typeof filterSearch === 'function') { return filterSearch(searchValue, filter) ? item : null; } return searchValueMatched(searchValue, filter.text) ? item : null; } return item; }); } export interface FilterDropdownProps { tablePrefixCls: string; prefixCls: string; dropdownPrefixCls: string; column: ColumnType; filterState?: FilterState; filterMultiple: boolean; filterMode?: 'menu' | 'tree'; filterSearch?: FilterSearchType; columnKey: Key; children: React.ReactNode; triggerFilter: (filterState: FilterState) => void; locale: TableLocale; getPopupContainer?: GetPopupContainer; filterResetToDefaultFilteredValue?: boolean; } function FilterDropdown(props: FilterDropdownProps) { const { tablePrefixCls, prefixCls, column, dropdownPrefixCls, columnKey, filterMultiple, filterMode = 'menu', filterSearch = false, filterState, triggerFilter, locale, children, getPopupContainer, } = props; const { filterDropdownVisible, onFilterDropdownVisibleChange, filterResetToDefaultFilteredValue, defaultFilteredValue, } = column; const [visible, setVisible] = React.useState(false); const filtered: boolean = !!( filterState && (filterState.filteredKeys?.length || filterState.forceFiltered) ); const triggerVisible = (newVisible: boolean) => { setVisible(newVisible); onFilterDropdownVisibleChange?.(newVisible); }; const mergedVisible = typeof filterDropdownVisible === 'boolean' ? filterDropdownVisible : visible; // ===================== Select Keys ===================== const propFilteredKeys = filterState?.filteredKeys; const [getFilteredKeysSync, setFilteredKeysSync] = useSyncState(propFilteredKeys || []); const onSelectKeys = ({ selectedKeys }: { selectedKeys: Key[] }) => { setFilteredKeysSync(selectedKeys); }; const onCheck = ( keys: Key[], { node, checked }: { node: EventDataNode; checked: boolean }, ) => { if (!filterMultiple) { onSelectKeys({ selectedKeys: checked && node.key ? [node.key] : [] }); } else { onSelectKeys({ selectedKeys: keys as Key[] }); } }; React.useEffect(() => { if (!visible) { return; } onSelectKeys({ selectedKeys: propFilteredKeys || [] }); }, [propFilteredKeys]); // ====================== Open Keys ====================== const [openKeys, setOpenKeys] = React.useState([]); const onOpenChange = (keys: string[]) => { setOpenKeys(keys); }; // search in tree mode column filter const [searchValue, setSearchValue] = React.useState(''); const onSearch = (e: React.ChangeEvent) => { const { value } = e.target; setSearchValue(value); }; // clear search value after close filter dropdown React.useEffect(() => { if (!visible) { setSearchValue(''); } }, [visible]); // ======================= Submit ======================== const internalTriggerFilter = (keys: Key[] | undefined | null) => { const mergedKeys = keys && keys.length ? keys : null; if (mergedKeys === null && (!filterState || !filterState.filteredKeys)) { return null; } if (isEqual(mergedKeys, filterState?.filteredKeys)) { return null; } triggerFilter({ column, key: columnKey, filteredKeys: mergedKeys, }); }; const onConfirm = () => { triggerVisible(false); internalTriggerFilter(getFilteredKeysSync()); }; const onReset = ( { confirm, closeDropdown }: FilterRestProps = { confirm: false, closeDropdown: false }, ) => { if (confirm) { internalTriggerFilter([]); } if (closeDropdown) { triggerVisible(false); } setSearchValue(''); if (filterResetToDefaultFilteredValue) { setFilteredKeysSync((defaultFilteredValue || []).map(key => String(key))); } else { setFilteredKeysSync([]); } }; const doFilter = ({ closeDropdown } = { closeDropdown: true }) => { if (closeDropdown) { triggerVisible(false); } internalTriggerFilter(getFilteredKeysSync()); }; const onVisibleChange = (newVisible: boolean) => { if (newVisible && propFilteredKeys !== undefined) { // Sync filteredKeys on appear in controlled mode (propFilteredKeys !== undefiend) setFilteredKeysSync(propFilteredKeys || []); } triggerVisible(newVisible); // Default will filter when closed if (!newVisible && !column.filterDropdown) { onConfirm(); } }; // ======================== Style ======================== const dropdownMenuClass = classNames({ [`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []), }); const onCheckAll = (e: CheckboxChangeEvent) => { if (e.target.checked) { const allFilterKeys = flattenKeys(column?.filters).map(key => String(key)); setFilteredKeysSync(allFilterKeys); } else { setFilteredKeysSync([]); } }; const getTreeData = ({ filters }: { filters?: ColumnFilterItem[] }) => (filters || []).map((filter, index) => { const key = String(filter.value); const item: FilterTreeDataNode = { title: filter.text, key: filter.value !== undefined ? key : index, }; if (filter.children) { item.children = getTreeData({ filters: filter.children }); } return item; }); let dropdownContent: React.ReactNode; if (typeof column.filterDropdown === 'function') { dropdownContent = column.filterDropdown({ prefixCls: `${dropdownPrefixCls}-custom`, setSelectedKeys: (selectedKeys: Key[]) => onSelectKeys({ selectedKeys }), selectedKeys: getFilteredKeysSync(), confirm: doFilter, clearFilters: onReset, filters: column.filters, visible: mergedVisible, }); } else if (column.filterDropdown) { dropdownContent = column.filterDropdown; } else { const selectedKeys = (getFilteredKeysSync() || []) as any; const getFilterComponent = () => { if ((column.filters || []).length === 0) { return ( ); } if (filterMode === 'tree') { return ( <>
{filterMultiple ? ( 0 && selectedKeys.length < flattenKeys(column.filters).length } className={`${tablePrefixCls}-filter-dropdown-checkall`} onChange={onCheckAll} > {locale.filterCheckall} ) : null} checkable selectable={false} blockNode multiple={filterMultiple} checkStrictly={!filterMultiple} className={`${dropdownPrefixCls}-menu`} onCheck={onCheck} checkedKeys={selectedKeys} selectedKeys={selectedKeys} showIcon={false} treeData={getTreeData({ filters: column.filters })} autoExpandParent defaultExpandAll filterTreeNode={ searchValue.trim() ? node => searchValueMatched(searchValue, node.title) : undefined } />
); } return ( <> ); }; const getResetDisabled = () => { if (filterResetToDefaultFilteredValue) { return isEqual( (defaultFilteredValue || []).map(key => String(key)), selectedKeys, ); } return selectedKeys.length === 0; }; dropdownContent = ( <> {getFilterComponent()}
); } // We should not block customize Menu with additional props if (column.filterDropdown) { dropdownContent = {dropdownContent}; } const menu = ( {dropdownContent} ); let filterIcon: React.ReactNode; if (typeof column.filterIcon === 'function') { filterIcon = column.filterIcon(filtered); } else if (column.filterIcon) { filterIcon = column.filterIcon; } else { filterIcon = ; } const { direction } = React.useContext(ConfigContext); return (
{children} { e.stopPropagation(); }} > {filterIcon}
); } export default FilterDropdown;