From 79ff7ac2dba4ab5cf01241ceef072f2c4be20e12 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Mon, 7 Mar 2022 22:24:38 +0800 Subject: [PATCH] feat: table add tree filter --- components/_util/EventInterface.ts | 5 + components/style/themes/default.less | 2 + components/table/Table.tsx | 6 +- .../table/hooks/useFilter/FilterDropdown.tsx | 249 +++++++++++++----- .../table/hooks/useFilter/FilterSearch.tsx | 38 +++ components/table/hooks/useFilter/index.tsx | 6 +- components/table/hooks/usePagination.ts | 4 +- components/table/hooks/useSelection.tsx | 172 ++++++++---- components/table/index.tsx | 11 +- components/table/interface.tsx | 5 +- components/table/style/bordered.less | 14 +- components/table/style/index.less | 88 ++++++- components/table/style/index.tsx | 2 + components/table/style/resize.less | 28 -- components/table/style/rtl.less | 14 +- components/table/style/size.less | 4 +- components/vc-table/Body/BodyRow.tsx | 19 +- components/vc-table/Body/ExpandedRow.tsx | 37 +-- components/vc-table/Body/MeasureCell.tsx | 2 +- components/vc-table/Body/index.tsx | 30 ++- components/vc-table/Cell/index.tsx | 94 +++++-- components/vc-table/ColGroup.tsx | 3 +- components/vc-table/Footer/Cell.tsx | 6 +- components/vc-table/Table.tsx | 35 ++- components/vc-table/constant.ts | 1 + components/vc-table/context/BodyContext.tsx | 4 - .../vc-table/context/ExpandedRowContext.tsx | 18 ++ components/vc-table/context/HoverContext.tsx | 21 ++ components/vc-table/context/StickyContext.tsx | 13 + components/vc-table/hooks/useColumns.tsx | 50 +++- .../vc-table/hooks/useFlattenRecords.ts | 14 +- components/vc-table/index.ts | 13 +- components/vc-table/interface.ts | 9 +- components/vc-table/stickyScrollBar.tsx | 3 + components/vc-table/utils/legacyUtil.ts | 50 ++-- 35 files changed, 770 insertions(+), 300 deletions(-) create mode 100644 components/table/hooks/useFilter/FilterSearch.tsx delete mode 100644 components/table/style/resize.less create mode 100644 components/vc-table/constant.ts create mode 100644 components/vc-table/context/ExpandedRowContext.tsx create mode 100644 components/vc-table/context/HoverContext.tsx create mode 100644 components/vc-table/context/StickyContext.tsx diff --git a/components/_util/EventInterface.ts b/components/_util/EventInterface.ts index b9d0bd019..7d6f193c9 100644 --- a/components/_util/EventInterface.ts +++ b/components/_util/EventInterface.ts @@ -6,5 +6,10 @@ export type ChangeEvent = Event & { value?: string | undefined; }; }; +export type CheckboxChangeEvent = Event & { + target: { + checked?: boolean; + }; +}; export type EventHandler = (...args: any[]) => void; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 1dfe4c221..8b54deeb9 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -632,6 +632,8 @@ // Sorter // Legacy: `table-header-sort-active-bg` is used for hover not real active @table-header-sort-active-bg: rgba(0, 0, 0, 0.04); +@table-fixed-header-sort-active-bg: hsv(0, 0, 96%); + // Filter @table-header-filter-active-bg: rgba(0, 0, 0, 0.04); @table-filter-btns-bg: inherit; diff --git a/components/table/Table.tsx b/components/table/Table.tsx index 4619d5472..607542fff 100644 --- a/components/table/Table.tsx +++ b/components/table/Table.tsx @@ -520,7 +520,6 @@ const InteralTable = defineComponent< expandType, childrenColumnName, locale: tableLocale, - expandIconColumnIndex, getPopupContainer: computed(() => props.getPopupContainer), }); @@ -581,8 +580,11 @@ const InteralTable = defineComponent< const renderPagination = (position: string) => ( ); diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx index 2c24aaf2d..92eba8ccf 100644 --- a/components/table/hooks/useFilter/FilterDropdown.tsx +++ b/components/table/hooks/useFilter/FilterDropdown.tsx @@ -15,10 +15,15 @@ import type { } from '../../interface'; import FilterDropdownMenuWrapper from './FilterWrapper'; import type { FilterState } from '.'; +import { flattenKeys } from '.'; import { computed, defineComponent, onBeforeUnmount, ref, shallowRef, watch } from 'vue'; import classNames from '../../../_util/classNames'; import useConfigInject from '../../../_util/hooks/useConfigInject'; import { useInjectSlots } from '../../context'; +import type { DataNode, EventDataNode } from '../../../tree'; +import type { CheckboxChangeEvent, EventHandler } from '../../../_util/EventInterface'; +import FilterSearch from './FilterSearch'; +import Tree from '../../../tree'; const { SubMenu, Item: MenuItem } = Menu; @@ -26,40 +31,26 @@ function hasSubMenu(filters: ColumnFilterItem[]) { return filters.some(({ children }) => children && children.length > 0); } +function searchValueMatched(searchValue: string, text: any) { + if (typeof text === 'string' || typeof text === 'number') { + return text?.toString().toLowerCase().includes(searchValue.trim().toLowerCase()); + } + return false; +} + function renderFilterItems({ filters, prefixCls, filteredKeys, filterMultiple, - locale, + searchValue, }: { filters: ColumnFilterItem[]; prefixCls: string; filteredKeys: Key[]; filterMultiple: boolean; - locale: TableLocale; + searchValue: string; }) { - if (filters.length === 0) { - // wrapped with
to avoid react warning - // https://github.com/ant-design/ant-design/issues/25979 - return ( - -
- -
-
- ); - } return filters.map((filter, index) => { const key = String(filter.value); @@ -75,7 +66,7 @@ function renderFilterItems({ prefixCls, filteredKeys, filterMultiple, - locale, + searchValue, })} ); @@ -83,12 +74,16 @@ function renderFilterItems({ const Component = filterMultiple ? Checkbox : Radio; - return ( + const item = ( {filter.text} ); + if (searchValue.trim()) { + return searchValueMatched(searchValue, filter.text) ? item : undefined; + } + return item; }); } @@ -99,6 +94,8 @@ export interface FilterDropdownProps { column: ColumnType; filterState?: FilterState; filterMultiple: boolean; + filterMode?: 'menu' | 'tree'; + filterSearch?: boolean; columnKey: Key; triggerFilter: (filterState: FilterState) => void; locale: TableLocale; @@ -114,6 +111,8 @@ export default defineComponent>({ 'column', 'filterState', 'filterMultiple', + 'filterMode', + 'filterSearch', 'columnKey', 'triggerFilter', 'locale', @@ -121,6 +120,8 @@ export default defineComponent>({ ] as any, setup(props, { slots }) { const contextSlots = useInjectSlots(); + const filterMode = computed(() => props.filterMode ?? 'menu'); + const filterSearch = computed(() => props.filterSearch ?? false); const filterDropdownVisible = computed(() => props.column.filterDropdownVisible); const visible = ref(false); const filtered = computed( @@ -168,9 +169,20 @@ export default defineComponent>({ filteredKeys.value = selectedKeys; }; + const onCheck = (keys: Key[], { node, checked }: { node: EventDataNode; checked: boolean }) => { + if (!props.filterMultiple) { + onSelectKeys({ selectedKeys: checked && node.key ? [node.key] : [] }); + } else { + onSelectKeys({ selectedKeys: keys as Key[] }); + } + }; + watch( propFilteredKeys, () => { + if (!visible.value) { + return; + } onSelectKeys({ selectedKeys: propFilteredKeys.value || [] }); }, { immediate: true }, @@ -193,6 +205,18 @@ export default defineComponent>({ clearTimeout(openRef.value); }); + const searchValue = ref(''); + const onSearch: EventHandler = e => { + const { value } = e.target; + searchValue.value = value; + }; + // clear search value after close filter dropdown + watch(visible, () => { + if (!visible.value) { + searchValue.value = ''; + } + }); + // ======================= Submit ======================== const internalTriggerFilter = (keys: Key[] | undefined | null) => { const { column, columnKey, filterState } = props; @@ -218,9 +242,8 @@ export default defineComponent>({ }; const onReset = () => { + searchValue.value = ''; filteredKeys.value = []; - triggerVisible(false); - internalTriggerFilter([]); }; const doFilter = ({ closeDropdown } = { closeDropdown: true }) => { @@ -244,20 +267,143 @@ export default defineComponent>({ }; const { direction } = useConfigInject('', props); - return () => { - const { - tablePrefixCls, - prefixCls, - column, - dropdownPrefixCls, - filterMultiple, - locale, - getPopupContainer, - } = props; - // ======================== Style ======================== - const dropdownMenuClass = classNames({ - [`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []), + + const onCheckAll = (e: CheckboxChangeEvent) => { + if (e.target.checked) { + const allFilterKeys = flattenKeys(props.column?.filters).map(key => String(key)); + filteredKeys.value = allFilterKeys; + } else { + filteredKeys.value = []; + } + }; + + const getTreeData = ({ filters }: { filters?: ColumnFilterItem[] }) => + (filters || []).map((filter, index) => { + const key = String(filter.value); + const item: DataNode = { + title: filter.text, + key: filter.value !== undefined ? key : index, + }; + if (filter.children) { + item.children = getTreeData({ filters: filter.children }); + } + return item; }); + // ======================== Style ======================== + const dropdownMenuClass = computed(() => + classNames({ + [`${props.dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu( + props.column.filters || [], + ), + }), + ); + const getFilterComponent = () => { + const selectedKeys = filteredKeys.value; + const { + column, + locale, + tablePrefixCls, + filterMultiple, + dropdownPrefixCls, + getPopupContainer, + prefixCls, + } = props; + if ((column.filters || []).length === 0) { + return ( + + ); + } + if (filterMode.value === 'tree') { + return ( + <> + +
+ {filterMultiple ? ( + + {locale.filterCheckall} + + ) : null} + searchValueMatched(searchValue.value, node.title) + : undefined + } + /> +
+ + ); + } + return ( + <> + + + renderFilterItems({ + filters: column.filters || [], + prefixCls, + filteredKeys: filteredKeys.value, + filterMultiple, + searchValue: searchValue.value, + }), + }} + > + + ); + }; + + return () => { + const { tablePrefixCls, prefixCls, column, dropdownPrefixCls, locale, getPopupContainer } = + props; let dropdownContent; @@ -278,28 +424,7 @@ export default defineComponent>({ const selectedKeys = filteredKeys.value as any; dropdownContent = ( <> - - renderFilterItems({ - filters: column.filters || [], - prefixCls, - filteredKeys: filteredKeys.value, - filterMultiple, - locale, - }), - }} - > + {getFilterComponent()}