feat: table add tree filter

feat-css-var
tangjinzhou 2022-03-07 22:24:38 +08:00
parent cc387b9bbd
commit 79ff7ac2db
35 changed files with 770 additions and 300 deletions

View File

@ -6,5 +6,10 @@ export type ChangeEvent = Event & {
value?: string | undefined;
};
};
export type CheckboxChangeEvent = Event & {
target: {
checked?: boolean;
};
};
export type EventHandler = (...args: any[]) => void;

View File

@ -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;

View File

@ -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) => (
<Pagination
class={`${prefixCls.value}-pagination ${prefixCls.value}-pagination-${position}`}
{...mergedPagination.value}
class={[
`${prefixCls.value}-pagination ${prefixCls.value}-pagination-${position}`,
mergedPagination.value.class,
]}
size={paginationSize}
/>
);

View File

@ -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 <div /> to avoid react warning
// https://github.com/ant-design/ant-design/issues/25979
return (
<MenuItem key="empty">
<div
style={{
margin: '16px 0',
}}
>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={locale.filterEmptyText}
imageStyle={{
height: 24,
}}
/>
</div>
</MenuItem>
);
}
return filters.map((filter, index) => {
const key = String(filter.value);
@ -75,7 +66,7 @@ function renderFilterItems({
prefixCls,
filteredKeys,
filterMultiple,
locale,
searchValue,
})}
</SubMenu>
);
@ -83,12 +74,16 @@ function renderFilterItems({
const Component = filterMultiple ? Checkbox : Radio;
return (
const item = (
<MenuItem key={filter.value !== undefined ? key : index}>
<Component checked={filteredKeys.includes(key)} />
<span>{filter.text}</span>
</MenuItem>
);
if (searchValue.trim()) {
return searchValueMatched(searchValue, filter.text) ? item : undefined;
}
return item;
});
}
@ -99,6 +94,8 @@ export interface FilterDropdownProps<RecordType> {
column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>;
filterMultiple: boolean;
filterMode?: 'menu' | 'tree';
filterSearch?: boolean;
columnKey: Key;
triggerFilter: (filterState: FilterState<RecordType>) => void;
locale: TableLocale;
@ -114,6 +111,8 @@ export default defineComponent<FilterDropdownProps<any>>({
'column',
'filterState',
'filterMultiple',
'filterMode',
'filterSearch',
'columnKey',
'triggerFilter',
'locale',
@ -121,6 +120,8 @@ export default defineComponent<FilterDropdownProps<any>>({
] 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<FilterDropdownProps<any>>({
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<FilterDropdownProps<any>>({
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<FilterDropdownProps<any>>({
};
const onReset = () => {
searchValue.value = '';
filteredKeys.value = [];
triggerVisible(false);
internalTriggerFilter([]);
};
const doFilter = ({ closeDropdown } = { closeDropdown: true }) => {
@ -244,20 +267,143 @@ export default defineComponent<FilterDropdownProps<any>>({
};
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 (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={locale.filterEmptyText}
imageStyle={{
height: 24,
}}
style={{
margin: 0,
padding: '16px 0',
}}
/>
);
}
if (filterMode.value === 'tree') {
return (
<>
<FilterSearch
filterSearch={filterSearch.value}
value={searchValue.value}
onChange={onSearch}
tablePrefixCls={tablePrefixCls}
locale={locale}
/>
<div class={`${tablePrefixCls}-filter-dropdown-tree`}>
{filterMultiple ? (
<Checkbox
class={`${tablePrefixCls}-filter-dropdown-checkall`}
onChange={onCheckAll}
>
{locale.filterCheckall}
</Checkbox>
) : null}
<Tree
checkable
selectable={false}
blockNode
multiple={filterMultiple}
checkStrictly={!filterMultiple}
class={`${dropdownPrefixCls}-menu`}
onCheck={onCheck}
checkedKeys={selectedKeys}
selectedKeys={selectedKeys}
showIcon={false}
treeData={getTreeData({ filters: column.filters })}
autoExpandParent
defaultExpandAll
filterTreeNode={
searchValue.value.trim()
? node => searchValueMatched(searchValue.value, node.title)
: undefined
}
/>
</div>
</>
);
}
return (
<>
<FilterSearch
filterSearch={filterSearch.value}
value={searchValue.value}
onChange={onSearch}
tablePrefixCls={tablePrefixCls}
locale={locale}
/>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
class={dropdownMenuClass.value}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={selectedKeys}
getPopupContainer={getPopupContainer}
openKeys={openKeys.value}
onOpenChange={onOpenChange}
v-slots={{
default: () =>
renderFilterItems({
filters: column.filters || [],
prefixCls,
filteredKeys: filteredKeys.value,
filterMultiple,
searchValue: searchValue.value,
}),
}}
></Menu>
</>
);
};
return () => {
const { tablePrefixCls, prefixCls, column, dropdownPrefixCls, locale, getPopupContainer } =
props;
let dropdownContent;
@ -278,28 +424,7 @@ export default defineComponent<FilterDropdownProps<any>>({
const selectedKeys = filteredKeys.value as any;
dropdownContent = (
<>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
class={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={selectedKeys}
getPopupContainer={getPopupContainer}
openKeys={openKeys.value}
onOpenChange={onOpenChange}
v-slots={{
default: () =>
renderFilterItems({
filters: column.filters || [],
prefixCls,
filteredKeys: filteredKeys.value,
filterMultiple,
locale,
}),
}}
></Menu>
{getFilterComponent()}
<div class={`${prefixCls}-dropdown-btns`}>
<Button
type="link"

View File

@ -0,0 +1,38 @@
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';
import type { TableLocale } from '../../interface';
import Input from '../../../input';
export default defineComponent({
name: 'FilterSearch',
inheritAttrs: false,
props: {
value: String,
onChange: Function as PropType<(e: InputEvent) => void>,
filterSearch: Boolean,
tablePrefixCls: String,
locale: { type: Object as PropType<TableLocale>, default: undefined as TableLocale },
},
setup(props) {
return () => {
const { value, onChange, filterSearch, tablePrefixCls, locale } = props;
if (!filterSearch) {
return null;
}
return (
<div class={`${tablePrefixCls}-filter-dropdown-search`}>
<Input
v-slots={{ prefix: () => <SearchOutlined /> }}
placeholder={locale.filterSearchPlaceholder}
onChange={onChange}
value={value}
// for skip min-width of input
htmlSize={1}
class={`${tablePrefixCls}-filter-dropdown-search-input`}
/>
</div>
);
};
},
});

View File

@ -81,7 +81,7 @@ function injectFilter<RecordType>(
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
const { filterMultiple = true } = column as ColumnType<RecordType>;
const { filterMultiple = true, filterMode, filterSearch } = column as ColumnType<RecordType>;
let newColumn: ColumnsType<RecordType>[number] = column;
const hasFilterDropdown =
@ -101,6 +101,8 @@ function injectFilter<RecordType>(
columnKey={columnKey}
filterState={filterState}
filterMultiple={filterMultiple}
filterMode={filterMode}
filterSearch={filterSearch}
triggerFilter={triggerFilter}
locale={locale}
getPopupContainer={getPopupContainer}
@ -131,7 +133,7 @@ function injectFilter<RecordType>(
});
}
function flattenKeys(filters?: ColumnFilterItem[]) {
export function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: FilterValue = [];
(filters || []).forEach(({ value, children }) => {
keys.push(value);

View File

@ -80,10 +80,10 @@ export default function usePagination(
return mP;
});
const refreshPagination = (current = 1, pageSize?: number) => {
const refreshPagination = (current?: number, pageSize?: number) => {
if (pagination.value === false) return;
setInnerPagination({
current,
current: current ?? 1,
pageSize: pageSize || mergedPagination.value.pageSize,
});
};

View File

@ -1,7 +1,7 @@
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import type { DataNode } from '../../tree';
import { INTERNAL_COL_DEFINE } from '../../vc-table';
import type { ColumnType, FixedType } from '../../vc-table/interface';
import type { FixedType } from '../../vc-table/interface';
import type { GetCheckDisabled } from '../../vc-tree/interface';
import { arrAdd, arrDel } from '../../vc-tree/util';
import { conductCheck } from '../../vc-tree/utils/conductUtil';
@ -10,7 +10,7 @@ import devWarning from '../../vc-util/devWarning';
import useMergedState from '../../_util/hooks/useMergedState';
import useState from '../../_util/hooks/useState';
import type { Ref } from 'vue';
import { computed, shallowRef } from 'vue';
import { watchEffect, computed, shallowRef } from 'vue';
import type { CheckboxProps } from '../../checkbox';
import Checkbox from '../../checkbox';
import Dropdown from '../../dropdown';
@ -20,6 +20,7 @@ import type {
TableRowSelection,
Key,
ColumnsType,
ColumnType,
GetRowKey,
TableLocale,
SelectionItem,
@ -29,14 +30,12 @@ import type {
} from '../interface';
// TODO: warning if use ajax!!!
export const SELECTION_COLUMN = {} as const;
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
export const SELECTION_NONE = 'SELECT_NONE' as const;
function getFixedType<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
return (column && column.fixed) as FixedType;
}
interface UseSelectionConfig<RecordType> {
prefixCls: Ref<string>;
pageData: Ref<RecordType[]>;
@ -45,7 +44,6 @@ interface UseSelectionConfig<RecordType> {
getRecordByKey: (key: Key) => RecordType;
expandType: Ref<ExpandType>;
childrenColumnName: Ref<string>;
expandIconColumnIndex?: Ref<number>;
locale: Ref<TableLocale>;
getPopupContainer?: Ref<GetPopupContainer>;
}
@ -79,13 +77,12 @@ export default function useSelection<RecordType>(
rowSelectionRef: Ref<TableRowSelection<RecordType> | undefined>,
configRef: UseSelectionConfig<RecordType>,
): [TransformColumns<RecordType>, Ref<Set<Key>>] {
// ======================== Caches ========================
const preserveRecordsRef = shallowRef(new Map<Key, RecordType>());
const mergedRowSelection = computed(() => {
const temp = rowSelectionRef.value || {};
const { checkStrictly = true } = temp;
return { ...temp, checkStrictly };
});
// ========================= Keys =========================
const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(
mergedRowSelection.value.selectedRowKeys ||
@ -96,6 +93,31 @@ export default function useSelection<RecordType>(
},
);
// ======================== Caches ========================
const preserveRecordsRef = shallowRef(new Map<Key, RecordType>());
const updatePreserveRecordsCache = (keys: Key[]) => {
if (mergedRowSelection.value.preserveSelectedRowKeys) {
const newCache = new Map<Key, RecordType>();
// Keep key if mark as preserveSelectedRowKeys
keys.forEach(key => {
let record = configRef.getRecordByKey(key);
if (!record && preserveRecordsRef.value.has(key)) {
record = preserveRecordsRef.value.get(key)!;
}
newCache.set(key, record);
});
// Refresh to new cache
preserveRecordsRef.value = newCache;
}
};
watchEffect(() => {
updatePreserveRecordsCache(mergedSelectedKeys.value);
});
const keyEntities = computed(() =>
mergedRowSelection.value.checkStrictly
? { keyEntities: null }
@ -179,26 +201,12 @@ export default function useSelection<RecordType>(
const setSelectedKeys = (keys: Key[]) => {
let availableKeys: Key[];
let records: RecordType[];
updatePreserveRecordsCache(keys);
const { preserveSelectedRowKeys, onChange: onSelectionChange } = mergedRowSelection.value;
const { getRecordByKey } = configRef;
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.value.has(key)) {
record = preserveRecordsRef.value.get(key)!;
}
newCache.set(key, record);
return record;
});
// Refresh to new cache
preserveRecordsRef.value = newCache;
records = keys.map(key => preserveRecordsRef.value.get(key)!);
} else {
// Filter key which not exist in the `dataSource`
availableKeys = [];
@ -249,7 +257,14 @@ export default function useSelection<RecordType>(
key: 'all',
text: tableLocale.value.selectionAll,
onSelect() {
setSelectedKeys(data.value.map((record, index) => getRowKey.value(record, index)));
setSelectedKeys(
data.value
.map((record, index) => getRowKey.value(record, index))
.filter(key => {
const checkProps = checkboxPropsMap.value.get(key);
return !checkProps?.disabled || derivedSelectedKeySet.value.has(key);
}),
);
},
};
}
@ -261,11 +276,13 @@ export default function useSelection<RecordType>(
const keySet = new Set(derivedSelectedKeySet.value);
pageData.value.forEach((record, index) => {
const key = getRowKey.value(record, index);
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
const checkProps = checkboxPropsMap.value.get(key);
if (!checkProps?.disabled) {
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
}
}
});
@ -289,7 +306,12 @@ export default function useSelection<RecordType>(
text: tableLocale.value.selectNone,
onSelect() {
onSelectNone?.();
setSelectedKeys([]);
setSelectedKeys(
Array.from(derivedSelectedKeySet.value).filter(key => {
const checkProps = checkboxPropsMap.value.get(key);
return checkProps?.disabled;
}),
);
},
};
}
@ -310,19 +332,21 @@ export default function useSelection<RecordType>(
checkStrictly,
} = mergedRowSelection.value;
const {
prefixCls,
getRecordByKey,
getRowKey,
expandType,
expandIconColumnIndex,
getPopupContainer,
} = configRef;
const { prefixCls, getRecordByKey, getRowKey, expandType, getPopupContainer } = configRef;
if (!rowSelectionRef.value) {
return columns;
if (process.env.NODE_ENV !== 'production') {
devWarning(
!columns.includes(SELECTION_COLUMN),
'Table',
'`rowSelection` is not config but `SELECTION_COLUMN` exists in the `columns`.',
);
}
return columns.filter(col => col !== SELECTION_COLUMN);
}
// Support selection
let cloneColumns = columns.slice();
const keySet = new Set(derivedSelectedKeySet.value);
// Record key only need check with enabled
@ -587,8 +611,62 @@ export default function useSelection<RecordType>(
return node;
};
// Columns
// Insert selection column if not exist
if (!cloneColumns.includes(SELECTION_COLUMN)) {
// Always after expand icon
if (
cloneColumns.findIndex(
(col: any) => col[INTERNAL_COL_DEFINE]?.columnType === 'EXPAND_COLUMN',
) === 0
) {
const [expandColumn, ...restColumns] = cloneColumns;
cloneColumns = [expandColumn, SELECTION_COLUMN, ...restColumns];
} else {
// Normal insert at first column
cloneColumns = [SELECTION_COLUMN, ...cloneColumns];
}
}
// Deduplicate selection column
const selectionColumnIndex = cloneColumns.indexOf(SELECTION_COLUMN);
if (
process.env.NODE_ENV !== 'production' &&
cloneColumns.filter(col => col === SELECTION_COLUMN).length > 1
) {
devWarning(false, 'Table', 'Multiple `SELECTION_COLUMN` exist in `columns`.');
}
cloneColumns = cloneColumns.filter(
(column, index) => column !== SELECTION_COLUMN || index === selectionColumnIndex,
);
// Fixed column logic
const prevCol: ColumnType<RecordType> & Record<string, any> =
cloneColumns[selectionColumnIndex - 1];
const nextCol: ColumnType<RecordType> & Record<string, any> =
cloneColumns[selectionColumnIndex + 1];
let mergedFixed: FixedType | undefined = fixed;
if (mergedFixed === undefined) {
if (nextCol?.fixed !== undefined) {
mergedFixed = nextCol.fixed;
} else if (prevCol?.fixed !== undefined) {
mergedFixed = prevCol.fixed;
}
}
if (
mergedFixed &&
prevCol &&
prevCol[INTERNAL_COL_DEFINE]?.columnType === 'EXPAND_COLUMN' &&
prevCol.fixed === undefined
) {
prevCol.fixed = mergedFixed;
}
// Replace with real selection column
const selectionColumn = {
fixed: mergedFixed,
width: selectionColWidth,
className: `${prefixCls.value}-selection-column`,
title: mergedRowSelection.value.columnTitle || title,
@ -598,15 +676,7 @@ export default function useSelection<RecordType>(
},
};
if (expandType.value === 'row' && columns.length && !expandIconColumnIndex.value) {
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];
return cloneColumns.map(col => (col === SELECTION_COLUMN ? selectionColumn : col));
};
return [transformColumns, derivedSelectedKeySet];

View File

@ -4,8 +4,13 @@ import ColumnGroup from './ColumnGroup';
import type { TableProps, TablePaginationConfig } from './Table';
import { defineComponent } from 'vue';
import type { App } from 'vue';
import { Summary, SummaryCell, SummaryRow } from '../vc-table';
import { SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE } from './hooks/useSelection';
import { EXPAND_COLUMN, Summary, SummaryCell, SummaryRow } from '../vc-table';
import {
SELECTION_ALL,
SELECTION_INVERT,
SELECTION_NONE,
SELECTION_COLUMN,
} from './hooks/useSelection';
export type { ColumnProps } from './Column';
export type { ColumnsType, ColumnType, ColumnGroupType } from './interface';
@ -34,6 +39,8 @@ export default Object.assign(Table, {
SELECTION_ALL,
SELECTION_INVERT,
SELECTION_NONE,
SELECTION_COLUMN,
EXPAND_COLUMN,
Column,
ColumnGroup,
Summary: TableSummary,

View File

@ -4,6 +4,7 @@ import type {
RenderedCell as RcRenderedCell,
ExpandableConfig,
DefaultRecordType,
FixedType,
} from '../vc-table/interface';
import type { TooltipProps } from '../tooltip';
import type { CheckboxProps } from '../checkbox';
@ -111,6 +112,8 @@ export interface ColumnType<RecordType = DefaultRecordType> extends RcColumnType
filteredValue?: FilterValue | null;
defaultFilteredValue?: FilterValue | null;
filterIcon?: VueNode | ((opt: { filtered: boolean; column: ColumnType }) => VueNode);
filterMode?: 'menu' | 'tree';
filterSearch?: boolean;
onFilter?: (value: string | number | boolean, record: RecordType) => boolean;
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;
@ -158,7 +161,7 @@ export interface TableRowSelection<T = DefaultRecordType> {
onSelectNone?: () => void;
selections?: INTERNAL_SELECTION_ITEM[] | boolean;
hideSelectAll?: boolean;
fixed?: boolean;
fixed?: FixedType;
columnWidth?: string | number;
columnTitle?: string | VueNode;
checkStrictly?: boolean;

View File

@ -1,5 +1,6 @@
@import './index';
@import './size';
@import (reference) '../../style/themes/index';
@table-prefix-cls: ~'@{ant-prefix}-table';
@table-border: @border-width-base @border-style-base @table-border-color;
@ -12,9 +13,7 @@
> .@{table-prefix-cls}-container {
// ============================ Content ============================
border: @table-border;
border-right: 0;
border-bottom: 0;
border-left: @table-border;
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-header,
@ -67,6 +66,13 @@
}
}
}
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-header {
> table {
border-top: @table-border;
}
}
}
&.@{table-prefix-cls}-scroll-horizontal {

View File

@ -2,15 +2,16 @@
@import '../../style/mixins/index';
@import './size';
@import './bordered';
@import './resize.less';
@table-prefix-cls: ~'@{ant-prefix}-table';
@tree-prefix-cls: ~'@{ant-prefix}-tree';
@dropdown-prefix-cls: ~'@{ant-prefix}-dropdown';
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
@table-header-icon-color: #bfbfbf;
@table-header-icon-color-hover: darken(@table-header-icon-color, 10%);
@table-sticky-zindex: (@zindex-table-fixed + 1);
@table-sticky-zindex: calc(@zindex-table-fixed + 1);
@table-sticky-scroll-bar-active-bg: fade(@table-sticky-scroll-bar-bg, 80%);
@table-filter-dropdown-max-height: 264px;
.@{table-prefix-cls}-wrapper {
clear: both;
@ -144,10 +145,9 @@
}
}
&.@{table-prefix-cls}-row:hover {
> td {
background: @table-row-hover-bg;
}
&.@{table-prefix-cls}-row:hover > td,
> td.@{table-prefix-cls}-cell-row-hover {
background: @table-row-hover-bg;
}
&.@{table-prefix-cls}-row-selected {
@ -167,6 +167,8 @@
// =========================== Summary ============================
&-summary {
position: relative;
z-index: @zindex-table-fixed;
background: @table-bg;
div& {
@ -228,7 +230,7 @@
// https://github.com/ant-design/ant-design/issues/30969
&.@{table-prefix-cls}-cell-fix-left:hover,
&.@{table-prefix-cls}-cell-fix-right:hover {
background: lighten(@black, 96%);
background: @table-fixed-header-sort-active-bg;
}
}
@ -269,6 +271,7 @@
}
&-column-sorter {
margin-left: 4px;
color: @table-header-icon-color;
font-size: 0;
transition: color 0.3s;
@ -329,21 +332,64 @@
&-filter-dropdown {
.reset-component();
min-width: 120px;
background-color: @table-filter-dropdown-bg;
border-radius: @border-radius-base;
box-shadow: @box-shadow-base;
// Reset menu
.@{dropdown-prefix-cls}-menu {
// https://github.com/ant-design/ant-design/issues/4916
// https://github.com/ant-design/ant-design/issues/19542
max-height: 264px;
max-height: @table-filter-dropdown-max-height;
overflow-x: hidden;
border: 0;
box-shadow: none;
&:empty::after {
display: block;
padding: 8px 0;
color: @disabled-color;
font-size: @font-size-sm;
text-align: center;
content: 'Not Found';
}
}
min-width: 120px;
background-color: @table-filter-dropdown-bg;
&-tree {
padding: 8px 8px 0;
border-radius: @border-radius-base;
box-shadow: @box-shadow-base;
.@{tree-prefix-cls}-treenode .@{tree-prefix-cls}-node-content-wrapper:hover {
background-color: @tree-node-hover-bg;
}
.@{tree-prefix-cls}-treenode-checkbox-checked .@{tree-prefix-cls}-node-content-wrapper {
&,
&:hover {
background-color: @tree-node-selected-bg;
}
}
}
&-search {
padding: 8px;
border-bottom: @border-width-base @border-color-split @border-style-base;
&-input {
input {
min-width: 140px;
}
.@{iconfont-css-prefix} {
color: @disabled-color;
}
}
}
&-checkall {
width: 100%;
margin-bottom: 4px;
margin-left: 4px;
}
&-submenu > ul {
max-height: calc(100vh - 130px);
@ -363,7 +409,7 @@
&-btns {
display: flex;
justify-content: space-between;
padding: 7px 8px 7px 3px;
padding: 7px 8px;
overflow: hidden;
background-color: @table-filter-btns-bg;
border-top: @border-width-base @border-style-base @table-border-color;
@ -390,6 +436,10 @@
}
}
table tr th&-selection-column&-cell-fix-left {
z-index: 3;
}
table tr th&-selection-column::after {
background-color: transparent !important;
}
@ -488,6 +538,7 @@
&-collapsed::before {
transform: rotate(-180deg);
}
&-collapsed::after {
transform: rotate(0deg);
}
@ -542,6 +593,7 @@
.@{table-prefix-cls}-empty & {
color: @disabled-color;
}
&:hover {
> td {
background: @component-background;
@ -552,7 +604,6 @@
// ============================ Fixed =============================
&-cell-fix-left,
&-cell-fix-right {
position: -webkit-sticky !important;
position: sticky !important;
z-index: @zindex-table-fixed;
background: @table-bg;
@ -600,6 +651,7 @@
&::before {
left: 0;
}
&::after {
right: 0;
}
@ -638,11 +690,14 @@
box-shadow: inset -10px 0 8px -8px darken(@shadow-color, 5%);
}
}
&-sticky {
&-holder {
position: sticky;
z-index: @table-sticky-zindex;
background: @component-background;
}
&-scroll {
position: sticky;
bottom: 0;
@ -652,16 +707,20 @@
background: lighten(@table-border-color, 80%);
border-top: 1px solid @table-border-color;
opacity: 0.6;
&:hover {
transform-origin: center bottom;
}
&-bar {
height: 8px;
background-color: @table-sticky-scroll-bar-bg;
border-radius: @table-sticky-scroll-bar-radius;
&:hover {
background-color: @table-sticky-scroll-bar-active-bg;
}
&-active {
background-color: @table-sticky-scroll-bar-active-bg;
}
@ -677,6 +736,7 @@
box-shadow: none !important;
}
}
&-ping-right {
.@{table-prefix-cls}-cell-fix-right-first::after {
box-shadow: none !important;

View File

@ -12,3 +12,5 @@ import '../../dropdown/style';
import '../../spin/style';
import '../../pagination/style';
import '../../tooltip/style';
import '../../input/style';
import '../../tree/style';

View File

@ -1,28 +0,0 @@
.@{table-prefix-cls}-resize-handle {
position: absolute;
top: 0;
height: 100% !important;
bottom: 0;
left: auto !important;
right: -8px;
cursor: col-resize;
touch-action: none;
user-select: auto;
width: 16px;
z-index: 1;
&-line {
display: block;
width: 1px;
margin-left: 7px;
height: 100% !important;
background-color: @primary-color;
opacity: 0;
}
&:hover &-line {
opacity: 1;
}
}
.dragging .@{table-prefix-cls}-resize-handle-line {
opacity: 1;
}

View File

@ -32,6 +32,13 @@
}
}
&:not(:last-child):not(.@{table-prefix-cls}-selection-column):not(.@{table-prefix-cls}-row-expand-icon-cell):not([colspan])::before {
.@{table-wrapepr-rtl-cls} & {
right: auto;
left: 0;
}
}
.@{table-wrapepr-rtl-cls} & {
text-align: right;
}
@ -73,7 +80,7 @@
// ============================ Sorter ============================
&-column-sorter {
.@{table-wrapepr-rtl-cls} & {
margin-right: @padding-xs;
margin-right: 4px;
margin-left: 0;
}
}
@ -93,10 +100,9 @@
}
}
&-filter-trigger-container {
&-filter-trigger {
.@{table-wrapepr-rtl-cls} & {
right: auto;
left: 0;
margin: -4px 4px -4px (-@table-padding-horizontal / 2);
}
}

View File

@ -1,4 +1,6 @@
@import './index';
@import (reference) '../../style/themes/index';
@table-prefix-cls: ~'@{ant-prefix}-table';
.table-size(@size, @padding-vertical, @padding-horizontal, @font-size) {
.@{table-prefix-cls}.@{table-prefix-cls}-@{size} {

View File

@ -7,10 +7,12 @@ import { useInjectTable } from '../context/TableContext';
import { useInjectBody } from '../context/BodyContext';
import classNames from '../../_util/classNames';
import { parseStyleText } from '../../_util/props-util';
import type { MouseEventHandler } from '../../_util/EventInterface';
export interface BodyRowProps<RecordType> {
record: RecordType;
index: number;
renderIndex: number;
recordKey: Key;
expandedKeys: Set<Key>;
rowComponent: CustomizeComponent;
@ -29,6 +31,7 @@ export default defineComponent<BodyRowProps<unknown>>({
props: [
'record',
'index',
'renderIndex',
'recordKey',
'expandedKeys',
'rowComponent',
@ -74,14 +77,12 @@ export default defineComponent<BodyRowProps<unknown>>({
() => props.customRow?.(props.record, props.index) || {},
);
const onClick = (event, ...args) => {
const onClick: MouseEventHandler = (event, ...args) => {
if (bodyContext.expandRowByClick && mergedExpandable.value) {
onInternalTriggerExpand(props.record, event);
}
if (additionalProps.value?.onClick) {
additionalProps.value.onClick(event, ...args);
}
additionalProps.value?.onClick?.(event, ...args);
};
const computeRowClassName = computed(() => {
@ -109,10 +110,6 @@ export default defineComponent<BodyRowProps<unknown>>({
} = props;
const { prefixCls, fixedInfoList, transformCellText } = tableContext;
const {
fixHeader,
fixColumn,
horizonScroll,
componentWidth,
flattenColumns,
expandedRowClassName,
indentSize,
@ -175,6 +172,7 @@ export default defineComponent<BodyRowProps<unknown>>({
key={key}
record={record}
index={index}
renderIndex={props.renderIndex}
dataIndex={dataIndex}
customRender={customRender}
{...fixedInfo}
@ -208,13 +206,10 @@ export default defineComponent<BodyRowProps<unknown>>({
computedExpandedRowClassName,
)}
prefixCls={prefixCls}
fixHeader={fixHeader}
fixColumn={fixColumn}
horizonScroll={horizonScroll}
component={RowComponent}
componentWidth={componentWidth}
cellComponent={cellComponent}
colSpan={flattenColumns.length}
isEmpty={false}
>
{expandContent}
</ExpandedRow>

View File

@ -2,46 +2,27 @@ import type { CustomizeComponent } from '../interface';
import Cell from '../Cell';
import { defineComponent } from 'vue';
import { useInjectTable } from '../context/TableContext';
import { useInjectExpandedRow } from '../context/ExpandedRowContext';
export interface ExpandedRowProps {
prefixCls: string;
component: CustomizeComponent;
cellComponent: CustomizeComponent;
fixHeader: boolean;
fixColumn: boolean;
horizonScroll: boolean;
componentWidth: number;
expanded: boolean;
colSpan: number;
isEmpty: boolean;
}
export default defineComponent<ExpandedRowProps>({
name: 'ExpandedRow',
inheritAttrs: false,
props: [
'prefixCls',
'component',
'cellComponent',
'fixHeader',
'fixColumn',
'horizonScroll',
'componentWidth',
'expanded',
'colSpan',
] as any,
props: ['prefixCls', 'component', 'cellComponent', 'expanded', 'colSpan', 'isEmpty'] as any,
setup(props, { slots, attrs }) {
const tableContext = useInjectTable();
const expandedRowContext = useInjectExpandedRow();
const { fixHeader, fixColumn, componentWidth, horizonScroll } = expandedRowContext;
return () => {
const {
prefixCls,
component: Component,
cellComponent,
fixHeader,
fixColumn,
expanded,
componentWidth,
colSpan,
} = props;
const { prefixCls, component: Component, cellComponent, expanded, colSpan, isEmpty } = props;
return (
<Component
@ -58,11 +39,13 @@ export default defineComponent<ExpandedRowProps>({
default: () => {
let contentNode: any = slots.default?.();
if (fixColumn) {
if (isEmpty ? horizonScroll.value : fixColumn.value) {
contentNode = (
<div
style={{
width: `${componentWidth - (fixHeader ? tableContext.scrollbarSize : 0)}px`,
width: `${
componentWidth.value - (fixHeader.value ? tableContext.scrollbarSize : 0)
}px`,
position: 'sticky',
left: 0,
overflow: 'hidden',

View File

@ -11,7 +11,7 @@ export default defineComponent<MeasureCellProps>({
name: 'MeasureCell',
props: ['columnKey'] as any,
setup(props, { emit }) {
const tdRef = ref();
const tdRef = ref<HTMLTableCellElement>();
onMounted(() => {
if (tdRef.value) {
emit('columnResize', props.columnKey, tdRef.value.offsetWidth);

View File

@ -5,10 +5,11 @@ import { getColumnsKey } from '../utils/valueUtil';
import MeasureCell from './MeasureCell';
import BodyRow from './BodyRow';
import useFlattenRecords from '../hooks/useFlattenRecords';
import { defineComponent, toRef } from 'vue';
import { defineComponent, ref, toRef } from 'vue';
import { useInjectResize } from '../context/ResizeContext';
import { useInjectTable } from '../context/TableContext';
import { useInjectBody } from '../context/BodyContext';
import { useProvideHover } from '../context/HoverContext';
export interface BodyProps<RecordType> {
data: RecordType[];
@ -43,7 +44,16 @@ export default defineComponent<BodyProps<any>>({
toRef(props, 'expandedKeys'),
toRef(props, 'getRowKey'),
);
const startRow = ref(-1);
const endRow = ref(-1);
useProvideHover({
startRow,
endRow,
onHover: (start, end) => {
startRow.value = start;
endRow.value = end;
},
});
return () => {
const {
data,
@ -56,17 +66,17 @@ export default defineComponent<BodyProps<any>>({
} = props;
const { onColumnResize } = resizeContext;
const { prefixCls, getComponent } = tableContext;
const { fixHeader, horizonScroll, flattenColumns, componentWidth } = bodyContext;
const { flattenColumns } = bodyContext;
const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody');
const trComponent = getComponent(['body', 'row'], 'tr');
const tdComponent = getComponent(['body', 'cell'], 'td');
let rows;
if (data.length) {
rows = flattenData.value.map((item, index) => {
const { record, indent } = item;
rows = flattenData.value.map((item, idx) => {
const { record, indent, index: renderIndex } = item;
const key = getRowKey(record, index);
const key = getRowKey(record, idx);
return (
<BodyRow
@ -74,7 +84,8 @@ export default defineComponent<BodyProps<any>>({
rowKey={key}
record={record}
recordKey={key}
index={index}
index={idx}
renderIndex={renderIndex}
rowComponent={trComponent}
cellComponent={tdComponent}
expandedKeys={expandedKeys}
@ -92,13 +103,10 @@ export default defineComponent<BodyProps<any>>({
expanded
class={`${prefixCls}-placeholder`}
prefixCls={prefixCls}
fixHeader={fixHeader}
fixColumn={horizonScroll}
horizonScroll={horizonScroll}
component={trComponent}
componentWidth={componentWidth}
cellComponent={tdComponent}
colSpan={flattenColumns.length}
isEmpty
>
{slots.emptyNode?.()}
</ExpandedRow>

View File

@ -1,7 +1,7 @@
import classNames from '../../_util/classNames';
import { flattenChildren, isValidElement, parseStyleText } from '../../_util/props-util';
import type { CSSProperties, HTMLAttributes } from 'vue';
import { defineComponent, isVNode, renderSlot } from 'vue';
import type { CSSProperties, TdHTMLAttributes } from 'vue';
import { computed, defineComponent, isVNode, renderSlot } from 'vue';
import type {
DataIndex,
@ -17,6 +17,16 @@ import type {
import { getPathValue, validateValue } from '../utils/valueUtil';
import { useInjectSlots } from '../../table/context';
import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil';
import { useInjectHover } from '../context/HoverContext';
import { useInjectSticky } from '../context/StickyContext';
import { warning } from '../../vc-util/warning';
import type { MouseEventHandler } from '../../_util/EventInterface';
/** Check if cell is in hover range */
function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) {
const cellEndRow = cellStartRow + cellRowSpan - 1;
return cellStartRow <= endRow && cellEndRow >= startRow;
}
function isRenderCell<RecordType = DefaultRecordType>(
data: RenderedCell<RecordType>,
@ -27,8 +37,10 @@ function isRenderCell<RecordType = DefaultRecordType>(
export interface CellProps<RecordType = DefaultRecordType> {
prefixCls?: string;
record?: RecordType;
/** `record` index. Not `column` index. */
/** `column` index is the real show rowIndex */
index?: number;
/** the index of the record. For the render(value, record, renderIndex) */
renderIndex?: number;
dataIndex?: DataIndex;
customRender?: ColumnType<RecordType>['customRender'];
component?: CustomizeComponent;
@ -49,7 +61,7 @@ export interface CellProps<RecordType = DefaultRecordType> {
/** @private Used for `expandable` with nest tree */
appendNode?: any;
additionalProps?: HTMLAttributes;
additionalProps?: TdHTMLAttributes;
rowType?: 'header' | 'body' | 'footer';
@ -67,6 +79,7 @@ export default defineComponent<CellProps>({
'prefixCls',
'record',
'index',
'renderIndex',
'dataIndex',
'customRender',
'component',
@ -91,16 +104,46 @@ export default defineComponent<CellProps>({
slots: ['appendNode'],
setup(props, { slots }) {
const contextSlots = useInjectSlots();
const { onHover, startRow, endRow } = useInjectHover();
const colSpan = computed(() => {
return props.colSpan ?? (props.additionalProps?.colspan as number);
});
const rowSpan = computed(() => {
return props.rowSpan ?? (props.additionalProps?.rowspan as number);
});
const hovering = computed(() => {
const { index } = props;
return inHoverRange(index, rowSpan.value || 1, startRow.value, endRow.value);
});
const supportSticky = useInjectSticky();
// ====================== Hover =======================
const onMouseenter = (event: MouseEvent, mergedRowSpan: number) => {
const { record, index, additionalProps } = props;
if (record) {
onHover(index, index + mergedRowSpan - 1);
}
additionalProps?.onMouseenter?.(event);
};
const onMouseleave: MouseEventHandler = event => {
const { record, additionalProps } = props;
if (record) {
onHover(-1, -1);
}
additionalProps?.onMouseleave?.(event);
};
return () => {
const {
prefixCls,
record,
index,
renderIndex,
dataIndex,
customRender,
component: Component = 'td',
colSpan,
rowSpan,
fixLeft,
fixRight,
firstFixLeft,
@ -135,10 +178,17 @@ export default defineComponent<CellProps>({
value,
record,
index,
renderIndex,
column: column.__originColumn__,
});
if (isRenderCell(renderData)) {
if (process.env.NODE_ENV !== 'production') {
warning(
false,
'`columns.customRender` return cell props is deprecated with perf issue, please use `customCell` instead.',
);
}
childNode = renderData.children;
cellProps = renderData.props;
} else {
@ -208,8 +258,8 @@ export default defineComponent<CellProps>({
class: cellClassName,
...restCellProps
} = cellProps || {};
const mergedColSpan = cellColSpan !== undefined ? cellColSpan : colSpan;
const mergedRowSpan = cellRowSpan !== undefined ? cellRowSpan : rowSpan;
const mergedColSpan = (cellColSpan !== undefined ? cellColSpan : colSpan.value) ?? 1;
const mergedRowSpan = (cellRowSpan !== undefined ? cellRowSpan : rowSpan.value) ?? 1;
if (mergedColSpan === 0 || mergedRowSpan === 0) {
return null;
@ -217,8 +267,8 @@ export default defineComponent<CellProps>({
// ====================== Fixed =======================
const fixedStyle: CSSProperties = {};
const isFixLeft = typeof fixLeft === 'number';
const isFixRight = typeof fixRight === 'number';
const isFixLeft = typeof fixLeft === 'number' && supportSticky.value;
const isFixRight = typeof fixRight === 'number' && supportSticky.value;
if (isFixLeft) {
fixedStyle.position = 'sticky';
@ -251,24 +301,30 @@ export default defineComponent<CellProps>({
title,
...restCellProps,
...additionalProps,
colSpan: mergedColSpan && mergedColSpan !== 1 ? mergedColSpan : null,
rowSpan: mergedRowSpan && mergedRowSpan !== 1 ? mergedRowSpan : null,
colSpan: mergedColSpan !== 1 ? mergedColSpan : null,
rowSpan: mergedRowSpan !== 1 ? mergedRowSpan : null,
class: classNames(
cellPrefixCls,
{
[`${cellPrefixCls}-fix-left`]: isFixLeft,
[`${cellPrefixCls}-fix-left-first`]: firstFixLeft,
[`${cellPrefixCls}-fix-left-last`]: lastFixLeft,
[`${cellPrefixCls}-fix-right`]: isFixRight,
[`${cellPrefixCls}-fix-right-first`]: firstFixRight,
[`${cellPrefixCls}-fix-right-last`]: lastFixRight,
[`${cellPrefixCls}-fix-left`]: isFixLeft && supportSticky.value,
[`${cellPrefixCls}-fix-left-first`]: firstFixLeft && supportSticky.value,
[`${cellPrefixCls}-fix-left-last`]: lastFixLeft && supportSticky.value,
[`${cellPrefixCls}-fix-right`]: isFixRight && supportSticky.value,
[`${cellPrefixCls}-fix-right-first`]: firstFixRight && supportSticky.value,
[`${cellPrefixCls}-fix-right-last`]: lastFixRight && supportSticky.value,
[`${cellPrefixCls}-ellipsis`]: ellipsis,
[`${cellPrefixCls}-with-append`]: appendNode,
[`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky,
[`${cellPrefixCls}-fix-sticky`]:
(isFixLeft || isFixRight) && isSticky && supportSticky.value,
[`${cellPrefixCls}-row-hover`]: !cellProps && hovering.value,
},
additionalProps.class,
cellClassName,
),
onMouseenter: (e: MouseEvent) => {
onMouseenter(e, mergedRowSpan);
},
onMouseleave,
style: {
...parseStyleText(additionalProps.style as any),
...alignStyle,

View File

@ -20,11 +20,12 @@ function ColGroup<RecordType>({ colWidths, columns, columCount }: ColGroupProps<
const additionalProps = column && column[INTERNAL_COL_DEFINE];
if (width || additionalProps || mustInsert) {
const { columnType, ...restAdditionalProps } = additionalProps || {};
cols.unshift(
<col
key={i}
style={{ width: typeof width === 'number' ? `${width}px` : width }}
{...additionalProps}
{...restAdditionalProps}
/>,
);
mustInsert = true;

View File

@ -41,12 +41,10 @@ export default defineComponent<SummaryCellProps>({
record={null}
dataIndex={null}
align={align}
colSpan={mergedColSpan}
rowSpan={rowSpan}
customRender={() => ({
children: slots.default?.(),
props: {
colSpan: mergedColSpan,
rowSpan,
},
})}
{...fixedInfo}
/>

View File

@ -57,7 +57,9 @@ import VCResizeObserver from '../vc-resize-observer';
import { useProvideTable } from './context/TableContext';
import { useProvideBody } from './context/BodyContext';
import { useProvideResize } from './context/ResizeContext';
import { getDataAndAriaProps } from './utils/legacyUtil';
import { useProvideSticky } from './context/StickyContext';
import pickAttrs from '../_util/pickAttrs';
import { useProvideExpandedRow } from './context/ExpandedRowContext';
// Used for conditions cache
const EMPTY_DATA = [];
@ -288,6 +290,17 @@ export default defineComponent<TableProps<DefaultRecordType>>({
emit('expandedRowsChange', newExpandedKeys);
};
// Warning if use `expandedRowRender` and nest children in the same time
if (
process.env.NODE_ENV !== 'production' &&
props.expandedRowRender &&
mergedData.value.some(record => {
return Array.isArray(record?.[mergedChildrenColumnName.value]);
})
) {
warning(false, '`expandedRowRender` should not use with nested Table');
}
const componentWidth = ref(0);
const [columns, flattenColumns] = useColumns(
@ -444,8 +457,11 @@ export default defineComponent<TableProps<DefaultRecordType>>({
};
const triggerOnScroll = () => {
if (scrollBodyRef.value) {
if (horizonScroll.value && scrollBodyRef.value) {
onScroll({ currentTarget: scrollBodyRef.value });
} else {
setPingedLeft(false);
setPingedRight(false);
}
};
let timtout;
@ -473,7 +489,7 @@ export default defineComponent<TableProps<DefaultRecordType>>({
});
const [scrollbarSize, setScrollbarSize] = useState(0);
useProvideSticky();
onMounted(() => {
nextTick(() => {
triggerOnScroll();
@ -554,10 +570,6 @@ export default defineComponent<TableProps<DefaultRecordType>>({
columns,
flattenColumns,
tableLayout: mergedTableLayout,
componentWidth,
fixHeader,
fixColumn,
horizonScroll,
expandIcon: mergedExpandIcon,
expandableType,
onTriggerExpand,
@ -568,6 +580,13 @@ export default defineComponent<TableProps<DefaultRecordType>>({
onColumnResize,
});
useProvideExpandedRow({
componentWidth,
fixHeader,
fixColumn,
horizonScroll,
});
// Body
const bodyTable = () => (
<Body
@ -773,7 +792,7 @@ export default defineComponent<TableProps<DefaultRecordType>>({
</div>
);
}
const ariaProps = getDataAndAriaProps(attrs);
const ariaProps = pickAttrs(attrs, { aria: true, data: true });
const fullTable = () => (
<div
{...ariaProps}

View File

@ -0,0 +1 @@
export const EXPAND_COLUMN = {} as const;

View File

@ -19,11 +19,7 @@ export interface BodyContextProps<RecordType = DefaultRecordType> {
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
componentWidth: number;
tableLayout: TableLayout;
fixHeader: boolean;
fixColumn: boolean;
horizonScroll: boolean;
indentSize: number;
expandableType: ExpandableType;

View File

@ -0,0 +1,18 @@
import type { InjectionKey, Ref } from 'vue';
import { inject, provide } from 'vue';
export interface ExpandedRowProps {
componentWidth: Ref<number>;
fixHeader: Ref<boolean>;
fixColumn: Ref<boolean>;
horizonScroll: Ref<boolean>;
}
export const ExpandedRowContextKey: InjectionKey<ExpandedRowProps> = Symbol('ExpandedRowProps');
export const useProvideExpandedRow = (props: ExpandedRowProps) => {
provide(ExpandedRowContextKey, props);
};
export const useInjectExpandedRow = () => {
return inject(ExpandedRowContextKey, {} as ExpandedRowProps);
};

View File

@ -0,0 +1,21 @@
import type { InjectionKey, Ref } from 'vue';
import { ref, inject, provide } from 'vue';
export interface HoverContextProps {
startRow: Ref<number>;
endRow: Ref<number>;
onHover: (start: number, end: number) => void;
}
export const HoverContextKey: InjectionKey<HoverContextProps> = Symbol('HoverContextProps');
export const useProvideHover = (props: HoverContextProps) => {
provide(HoverContextKey, props);
};
export const useInjectHover = () => {
return inject(HoverContextKey, {
startRow: ref(-1),
endRow: ref(-1),
onHover() {},
} as HoverContextProps);
};

View File

@ -0,0 +1,13 @@
import isStyleSupport from '../../_util/styleChecker';
import { onMounted, ref } from 'vue';
const supportSticky = ref(false);
export const useProvideSticky = () => {
onMounted(() => {
supportSticky.value = supportSticky.value || isStyleSupport('position', 'sticky');
});
};
export const useInjectSticky = () => {
return supportSticky;
};

View File

@ -12,6 +12,7 @@ import type {
ColumnGroupType,
} from '../interface';
import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil';
import { EXPAND_COLUMN } from '../constant';
function flatColumns<RecordType>(columns: ColumnsType<RecordType>): ColumnType<RecordType>[] {
return columns.reduce((list, column) => {
@ -121,8 +122,38 @@ function useColumns<RecordType>(
// Add expand column
const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
if (expandable.value) {
const expandColIndex = expandIconColumnIndex.value || 0;
const prevColumn = baseColumns.value[expandColIndex];
let cloneColumns = baseColumns.value.slice();
// >>> Warning if use `expandIconColumnIndex`
if (process.env.NODE_ENV !== 'production' && expandIconColumnIndex.value >= 0) {
warning(
false,
'`expandIconColumnIndex` is deprecated. Please use `Table.EXPAND_COLUMN` in `columns` instead.',
);
}
// >>> Insert expand column if not exist
if (!cloneColumns.includes(EXPAND_COLUMN)) {
const expandColIndex = expandIconColumnIndex.value || 0;
if (expandColIndex >= 0) {
cloneColumns.splice(expandColIndex, 0, EXPAND_COLUMN);
}
}
// >>> Deduplicate additional expand column
if (
process.env.NODE_ENV !== 'production' &&
cloneColumns.filter(c => c === EXPAND_COLUMN).length > 1
) {
warning(false, 'There exist more than one `EXPAND_COLUMN` in `columns`.');
}
const expandColumnIndex = cloneColumns.indexOf(EXPAND_COLUMN);
cloneColumns = cloneColumns.filter(
(column, index) => column !== EXPAND_COLUMN || index === expandColumnIndex,
);
// >>> Check if expand column need to fixed
const prevColumn = baseColumns.value[expandColumnIndex];
let fixedColumn: FixedType | null;
if ((expandFixed.value === 'left' || expandFixed.value) && !expandIconColumnIndex.value) {
@ -140,9 +171,11 @@ function useColumns<RecordType>(
const expandIconValue = expandIcon.value;
const prefixClsValue = prefixCls.value;
const expandRowByClickValue = expandRowByClick.value;
// >>> Create expandable column
const expandColumn = {
[INTERNAL_COL_DEFINE]: {
class: `${prefixCls.value}-expand-icon-col`,
columnType: 'EXPAND_COLUMN',
},
title: '',
fixed: fixedColumn,
@ -168,14 +201,13 @@ function useColumns<RecordType>(
},
};
// Insert expand column in the target position
const cloneColumns = baseColumns.value.slice();
if (expandColIndex >= 0) {
cloneColumns.splice(expandColIndex, 0, expandColumn);
}
return cloneColumns;
return cloneColumns.map(col => (col === EXPAND_COLUMN ? expandColumn : col));
}
return baseColumns.value;
if (process.env.NODE_ENV !== 'production' && baseColumns.value.includes(EXPAND_COLUMN)) {
warning(false, '`expandable` is not config but there exist `EXPAND_COLUMN` in `columns`.');
}
return baseColumns.value.filter(col => col !== EXPAND_COLUMN);
});
const mergedColumns = computed(() => {

View File

@ -9,12 +9,14 @@ function flatRecord<T>(
childrenColumnName: string,
expandedKeys: Set<Key>,
getRowKey: GetRowKey<T>,
index: number,
) {
const arr = [];
arr.push({
record,
indent,
index,
});
const key = getRowKey(record);
@ -30,6 +32,7 @@ function flatRecord<T>(
childrenColumnName,
expandedKeys,
getRowKey,
i,
);
arr.push(...tempArr);
@ -56,27 +59,30 @@ export default function useFlattenRecords<T = unknown>(
expandedKeysRef: Ref<Set<Key>>,
getRowKey: Ref<GetRowKey<T>>,
) {
const arr: Ref<{ record: T; indent: number }[]> = computed(() => {
const arr: Ref<{ record: T; indent: number; index: number }[]> = computed(() => {
const childrenColumnName = childrenColumnNameRef.value;
const expandedKeys = expandedKeysRef.value;
const data = dataRef.value;
if (expandedKeys?.size) {
const temp: { record: T; indent: number }[] = [];
const temp: { record: T; indent: number; index: number }[] = [];
// collect flattened record
for (let i = 0; i < data?.length; i += 1) {
const record = data[i];
temp.push(...flatRecord<T>(record, 0, childrenColumnName, expandedKeys, getRowKey.value));
temp.push(
...flatRecord<T>(record, 0, childrenColumnName, expandedKeys, getRowKey.value, i),
);
}
return temp;
}
return data?.map(item => {
return data?.map((item, index) => {
return {
record: item,
indent: 0,
index,
};
});
});

View File

@ -1,10 +1,19 @@
// base rc-table@7.17.2
// base rc-table@7.22.2
import Table from './Table';
import { FooterComponents as Summary, SummaryCell, SummaryRow } from './Footer';
import Column from './sugar/Column';
import ColumnGroup from './sugar/ColumnGroup';
import { INTERNAL_COL_DEFINE } from './utils/legacyUtil';
import { EXPAND_COLUMN } from './constant';
export { Summary, Column, ColumnGroup, SummaryCell, SummaryRow, INTERNAL_COL_DEFINE };
export {
Summary,
Column,
ColumnGroup,
SummaryCell,
SummaryRow,
INTERNAL_COL_DEFINE,
EXPAND_COLUMN,
};
export default Table;

View File

@ -15,7 +15,7 @@
* - onFilterDropdownVisibleChange
*/
import type { CSSProperties, HTMLAttributes, Ref } from 'vue';
import type { CSSProperties, HTMLAttributes, Ref, TdHTMLAttributes } from 'vue';
export type Key = number | string;
@ -106,6 +106,7 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
text: any; // 兼容 V2
record: RecordType;
index: number;
renderIndex: number;
column: ColumnType<RecordType>;
}) => any | RenderedCell<RecordType>;
rowSpan?: number;
@ -114,7 +115,7 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
maxWidth?: number;
resizable?: boolean;
customCell?: GetComponentProps<RecordType>;
/** @deprecated Please use `onCell` instead */
/** @deprecated Please use `customCell` instead */
onCellClick?: (record: RecordType, e: MouseEvent) => void;
}
@ -137,7 +138,7 @@ export type GetComponentProps<DataType> = (
data: DataType,
index?: number,
column?: ColumnType<any>,
) => Omit<HTMLAttributes, 'style'> & { style?: CSSProperties };
) => Omit<HTMLAttributes | TdHTMLAttributes, 'style'> & { style?: CSSProperties };
// type Component<P> = DefineComponent<P> | FunctionalComponent<P> | string;
@ -231,7 +232,9 @@ export interface ExpandableConfig<RecordType> {
onExpandedRowsChange?: (expandedKeys: readonly Key[]) => void;
defaultExpandAllRows?: boolean;
indentSize?: number;
/** @deprecated Please use `EXPAND_COLUMN` in `columns` directly */
expandIconColumnIndex?: number;
showExpandColumn?: boolean;
expandedRowClassName?: RowClassName<RecordType>;
childrenColumnName?: string;
rowExpandable?: (record: RecordType) => boolean;

View File

@ -99,6 +99,9 @@ export default defineComponent<StickyScrollBarProps>({
};
const onContainerScroll = () => {
if (!props.scrollBodyRef.value) {
return;
}
const tableOffsetTop = getOffset(props.scrollBodyRef.value).top;
const tableBottomOffset = tableOffsetTop + props.scrollBodyRef.value.offsetHeight;
const currentClientOffset =

View File

@ -9,34 +9,40 @@ export function getExpandableProps<RecordType>(
},
): ExpandableConfig<RecordType> {
const { expandable, ...legacyExpandableConfig } = props;
let config: ExpandableConfig<RecordType>;
if (props.expandable !== undefined) {
return {
config = {
...legacyExpandableConfig,
...expandable,
};
} else {
if (
process.env.NODE_ENV !== 'production' &&
[
'indentSize',
'expandedRowKeys',
'defaultExpandedRowKeys',
'defaultExpandAllRows',
'expandedRowRender',
'expandRowByClick',
'expandIcon',
'onExpand',
'onExpandedRowsChange',
'expandedRowClassName',
'expandIconColumnIndex',
'showExpandColumn',
].some(prop => prop in props)
) {
warning(false, 'expanded related props have been moved into `expandable`.');
}
config = legacyExpandableConfig;
}
if (config.showExpandColumn === false) {
config.expandIconColumnIndex = -1;
}
if (
process.env.NODE_ENV !== 'production' &&
[
'indentSize',
'expandedRowKeys',
'defaultExpandedRowKeys',
'defaultExpandAllRows',
'expandedRowRender',
'expandRowByClick',
'expandIcon',
'onExpand',
'onExpandedRowsChange',
'expandedRowClassName',
'expandIconColumnIndex',
].some(prop => prop in props)
) {
warning(false, 'expanded related props have been moved into `expandable`.');
}
return legacyExpandableConfig;
return config;
}
/**