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; value?: string | undefined;
}; };
}; };
export type CheckboxChangeEvent = Event & {
target: {
checked?: boolean;
};
};
export type EventHandler = (...args: any[]) => void; export type EventHandler = (...args: any[]) => void;

View File

@ -632,6 +632,8 @@
// Sorter // Sorter
// Legacy: `table-header-sort-active-bg` is used for hover not real active // 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-header-sort-active-bg: rgba(0, 0, 0, 0.04);
@table-fixed-header-sort-active-bg: hsv(0, 0, 96%);
// Filter // Filter
@table-header-filter-active-bg: rgba(0, 0, 0, 0.04); @table-header-filter-active-bg: rgba(0, 0, 0, 0.04);
@table-filter-btns-bg: inherit; @table-filter-btns-bg: inherit;

View File

@ -520,7 +520,6 @@ const InteralTable = defineComponent<
expandType, expandType,
childrenColumnName, childrenColumnName,
locale: tableLocale, locale: tableLocale,
expandIconColumnIndex,
getPopupContainer: computed(() => props.getPopupContainer), getPopupContainer: computed(() => props.getPopupContainer),
}); });
@ -581,8 +580,11 @@ const InteralTable = defineComponent<
const renderPagination = (position: string) => ( const renderPagination = (position: string) => (
<Pagination <Pagination
class={`${prefixCls.value}-pagination ${prefixCls.value}-pagination-${position}`}
{...mergedPagination.value} {...mergedPagination.value}
class={[
`${prefixCls.value}-pagination ${prefixCls.value}-pagination-${position}`,
mergedPagination.value.class,
]}
size={paginationSize} size={paginationSize}
/> />
); );

View File

@ -15,10 +15,15 @@ import type {
} from '../../interface'; } from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper'; import FilterDropdownMenuWrapper from './FilterWrapper';
import type { FilterState } from '.'; import type { FilterState } from '.';
import { flattenKeys } from '.';
import { computed, defineComponent, onBeforeUnmount, ref, shallowRef, watch } from 'vue'; import { computed, defineComponent, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
import classNames from '../../../_util/classNames'; import classNames from '../../../_util/classNames';
import useConfigInject from '../../../_util/hooks/useConfigInject'; import useConfigInject from '../../../_util/hooks/useConfigInject';
import { useInjectSlots } from '../../context'; 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; const { SubMenu, Item: MenuItem } = Menu;
@ -26,40 +31,26 @@ function hasSubMenu(filters: ColumnFilterItem[]) {
return filters.some(({ children }) => children && children.length > 0); 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({ function renderFilterItems({
filters, filters,
prefixCls, prefixCls,
filteredKeys, filteredKeys,
filterMultiple, filterMultiple,
locale, searchValue,
}: { }: {
filters: ColumnFilterItem[]; filters: ColumnFilterItem[];
prefixCls: string; prefixCls: string;
filteredKeys: Key[]; filteredKeys: Key[];
filterMultiple: boolean; 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) => { return filters.map((filter, index) => {
const key = String(filter.value); const key = String(filter.value);
@ -75,7 +66,7 @@ function renderFilterItems({
prefixCls, prefixCls,
filteredKeys, filteredKeys,
filterMultiple, filterMultiple,
locale, searchValue,
})} })}
</SubMenu> </SubMenu>
); );
@ -83,12 +74,16 @@ function renderFilterItems({
const Component = filterMultiple ? Checkbox : Radio; const Component = filterMultiple ? Checkbox : Radio;
return ( const item = (
<MenuItem key={filter.value !== undefined ? key : index}> <MenuItem key={filter.value !== undefined ? key : index}>
<Component checked={filteredKeys.includes(key)} /> <Component checked={filteredKeys.includes(key)} />
<span>{filter.text}</span> <span>{filter.text}</span>
</MenuItem> </MenuItem>
); );
if (searchValue.trim()) {
return searchValueMatched(searchValue, filter.text) ? item : undefined;
}
return item;
}); });
} }
@ -99,6 +94,8 @@ export interface FilterDropdownProps<RecordType> {
column: ColumnType<RecordType>; column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>; filterState?: FilterState<RecordType>;
filterMultiple: boolean; filterMultiple: boolean;
filterMode?: 'menu' | 'tree';
filterSearch?: boolean;
columnKey: Key; columnKey: Key;
triggerFilter: (filterState: FilterState<RecordType>) => void; triggerFilter: (filterState: FilterState<RecordType>) => void;
locale: TableLocale; locale: TableLocale;
@ -114,6 +111,8 @@ export default defineComponent<FilterDropdownProps<any>>({
'column', 'column',
'filterState', 'filterState',
'filterMultiple', 'filterMultiple',
'filterMode',
'filterSearch',
'columnKey', 'columnKey',
'triggerFilter', 'triggerFilter',
'locale', 'locale',
@ -121,6 +120,8 @@ export default defineComponent<FilterDropdownProps<any>>({
] as any, ] as any,
setup(props, { slots }) { setup(props, { slots }) {
const contextSlots = useInjectSlots(); const contextSlots = useInjectSlots();
const filterMode = computed(() => props.filterMode ?? 'menu');
const filterSearch = computed(() => props.filterSearch ?? false);
const filterDropdownVisible = computed(() => props.column.filterDropdownVisible); const filterDropdownVisible = computed(() => props.column.filterDropdownVisible);
const visible = ref(false); const visible = ref(false);
const filtered = computed( const filtered = computed(
@ -168,9 +169,20 @@ export default defineComponent<FilterDropdownProps<any>>({
filteredKeys.value = selectedKeys; 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( watch(
propFilteredKeys, propFilteredKeys,
() => { () => {
if (!visible.value) {
return;
}
onSelectKeys({ selectedKeys: propFilteredKeys.value || [] }); onSelectKeys({ selectedKeys: propFilteredKeys.value || [] });
}, },
{ immediate: true }, { immediate: true },
@ -193,6 +205,18 @@ export default defineComponent<FilterDropdownProps<any>>({
clearTimeout(openRef.value); 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 ======================== // ======================= Submit ========================
const internalTriggerFilter = (keys: Key[] | undefined | null) => { const internalTriggerFilter = (keys: Key[] | undefined | null) => {
const { column, columnKey, filterState } = props; const { column, columnKey, filterState } = props;
@ -218,9 +242,8 @@ export default defineComponent<FilterDropdownProps<any>>({
}; };
const onReset = () => { const onReset = () => {
searchValue.value = '';
filteredKeys.value = []; filteredKeys.value = [];
triggerVisible(false);
internalTriggerFilter([]);
}; };
const doFilter = ({ closeDropdown } = { closeDropdown: true }) => { const doFilter = ({ closeDropdown } = { closeDropdown: true }) => {
@ -244,20 +267,143 @@ export default defineComponent<FilterDropdownProps<any>>({
}; };
const { direction } = useConfigInject('', props); const { direction } = useConfigInject('', props);
return () => {
const { const onCheckAll = (e: CheckboxChangeEvent) => {
tablePrefixCls, if (e.target.checked) {
prefixCls, const allFilterKeys = flattenKeys(props.column?.filters).map(key => String(key));
column, filteredKeys.value = allFilterKeys;
dropdownPrefixCls, } else {
filterMultiple, filteredKeys.value = [];
locale, }
getPopupContainer, };
} = props;
// ======================== Style ======================== const getTreeData = ({ filters }: { filters?: ColumnFilterItem[] }) =>
const dropdownMenuClass = classNames({ (filters || []).map((filter, index) => {
[`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []), 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; let dropdownContent;
@ -278,28 +424,7 @@ export default defineComponent<FilterDropdownProps<any>>({
const selectedKeys = filteredKeys.value as any; const selectedKeys = filteredKeys.value as any;
dropdownContent = ( dropdownContent = (
<> <>
<Menu {getFilterComponent()}
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>
<div class={`${prefixCls}-dropdown-btns`}> <div class={`${prefixCls}-dropdown-btns`}>
<Button <Button
type="link" 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> { ): ColumnsType<RecordType> {
return columns.map((column, index) => { return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos); 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; let newColumn: ColumnsType<RecordType>[number] = column;
const hasFilterDropdown = const hasFilterDropdown =
@ -101,6 +101,8 @@ function injectFilter<RecordType>(
columnKey={columnKey} columnKey={columnKey}
filterState={filterState} filterState={filterState}
filterMultiple={filterMultiple} filterMultiple={filterMultiple}
filterMode={filterMode}
filterSearch={filterSearch}
triggerFilter={triggerFilter} triggerFilter={triggerFilter}
locale={locale} locale={locale}
getPopupContainer={getPopupContainer} getPopupContainer={getPopupContainer}
@ -131,7 +133,7 @@ function injectFilter<RecordType>(
}); });
} }
function flattenKeys(filters?: ColumnFilterItem[]) { export function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: FilterValue = []; let keys: FilterValue = [];
(filters || []).forEach(({ value, children }) => { (filters || []).forEach(({ value, children }) => {
keys.push(value); keys.push(value);

View File

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

View File

@ -1,7 +1,7 @@
import DownOutlined from '@ant-design/icons-vue/DownOutlined'; import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import type { DataNode } from '../../tree'; import type { DataNode } from '../../tree';
import { INTERNAL_COL_DEFINE } from '../../vc-table'; 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 type { GetCheckDisabled } from '../../vc-tree/interface';
import { arrAdd, arrDel } from '../../vc-tree/util'; import { arrAdd, arrDel } from '../../vc-tree/util';
import { conductCheck } from '../../vc-tree/utils/conductUtil'; import { conductCheck } from '../../vc-tree/utils/conductUtil';
@ -10,7 +10,7 @@ import devWarning from '../../vc-util/devWarning';
import useMergedState from '../../_util/hooks/useMergedState'; import useMergedState from '../../_util/hooks/useMergedState';
import useState from '../../_util/hooks/useState'; import useState from '../../_util/hooks/useState';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, shallowRef } from 'vue'; import { watchEffect, computed, shallowRef } from 'vue';
import type { CheckboxProps } from '../../checkbox'; import type { CheckboxProps } from '../../checkbox';
import Checkbox from '../../checkbox'; import Checkbox from '../../checkbox';
import Dropdown from '../../dropdown'; import Dropdown from '../../dropdown';
@ -20,6 +20,7 @@ import type {
TableRowSelection, TableRowSelection,
Key, Key,
ColumnsType, ColumnsType,
ColumnType,
GetRowKey, GetRowKey,
TableLocale, TableLocale,
SelectionItem, SelectionItem,
@ -29,14 +30,12 @@ import type {
} from '../interface'; } from '../interface';
// TODO: warning if use ajax!!! // TODO: warning if use ajax!!!
export const SELECTION_COLUMN = {} as const;
export const SELECTION_ALL = 'SELECT_ALL' as const; export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const; export const SELECTION_INVERT = 'SELECT_INVERT' as const;
export const SELECTION_NONE = 'SELECT_NONE' 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> { interface UseSelectionConfig<RecordType> {
prefixCls: Ref<string>; prefixCls: Ref<string>;
pageData: Ref<RecordType[]>; pageData: Ref<RecordType[]>;
@ -45,7 +44,6 @@ interface UseSelectionConfig<RecordType> {
getRecordByKey: (key: Key) => RecordType; getRecordByKey: (key: Key) => RecordType;
expandType: Ref<ExpandType>; expandType: Ref<ExpandType>;
childrenColumnName: Ref<string>; childrenColumnName: Ref<string>;
expandIconColumnIndex?: Ref<number>;
locale: Ref<TableLocale>; locale: Ref<TableLocale>;
getPopupContainer?: Ref<GetPopupContainer>; getPopupContainer?: Ref<GetPopupContainer>;
} }
@ -79,13 +77,12 @@ export default function useSelection<RecordType>(
rowSelectionRef: Ref<TableRowSelection<RecordType> | undefined>, rowSelectionRef: Ref<TableRowSelection<RecordType> | undefined>,
configRef: UseSelectionConfig<RecordType>, configRef: UseSelectionConfig<RecordType>,
): [TransformColumns<RecordType>, Ref<Set<Key>>] { ): [TransformColumns<RecordType>, Ref<Set<Key>>] {
// ======================== Caches ========================
const preserveRecordsRef = shallowRef(new Map<Key, RecordType>());
const mergedRowSelection = computed(() => { const mergedRowSelection = computed(() => {
const temp = rowSelectionRef.value || {}; const temp = rowSelectionRef.value || {};
const { checkStrictly = true } = temp; const { checkStrictly = true } = temp;
return { ...temp, checkStrictly }; return { ...temp, checkStrictly };
}); });
// ========================= Keys ========================= // ========================= Keys =========================
const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState( const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(
mergedRowSelection.value.selectedRowKeys || 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(() => const keyEntities = computed(() =>
mergedRowSelection.value.checkStrictly mergedRowSelection.value.checkStrictly
? { keyEntities: null } ? { keyEntities: null }
@ -179,26 +201,12 @@ export default function useSelection<RecordType>(
const setSelectedKeys = (keys: Key[]) => { const setSelectedKeys = (keys: Key[]) => {
let availableKeys: Key[]; let availableKeys: Key[];
let records: RecordType[]; let records: RecordType[];
updatePreserveRecordsCache(keys);
const { preserveSelectedRowKeys, onChange: onSelectionChange } = mergedRowSelection.value; const { preserveSelectedRowKeys, onChange: onSelectionChange } = mergedRowSelection.value;
const { getRecordByKey } = configRef; const { getRecordByKey } = configRef;
if (preserveSelectedRowKeys) { if (preserveSelectedRowKeys) {
// Keep key if mark as preserveSelectedRowKeys
const newCache = new Map<Key, RecordType>();
availableKeys = keys; availableKeys = keys;
records = keys.map(key => { records = keys.map(key => preserveRecordsRef.value.get(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;
} else { } else {
// Filter key which not exist in the `dataSource` // Filter key which not exist in the `dataSource`
availableKeys = []; availableKeys = [];
@ -249,7 +257,14 @@ export default function useSelection<RecordType>(
key: 'all', key: 'all',
text: tableLocale.value.selectionAll, text: tableLocale.value.selectionAll,
onSelect() { 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); const keySet = new Set(derivedSelectedKeySet.value);
pageData.value.forEach((record, index) => { pageData.value.forEach((record, index) => {
const key = getRowKey.value(record, index); const key = getRowKey.value(record, index);
const checkProps = checkboxPropsMap.value.get(key);
if (keySet.has(key)) { if (!checkProps?.disabled) {
keySet.delete(key); if (keySet.has(key)) {
} else { keySet.delete(key);
keySet.add(key); } else {
keySet.add(key);
}
} }
}); });
@ -289,7 +306,12 @@ export default function useSelection<RecordType>(
text: tableLocale.value.selectNone, text: tableLocale.value.selectNone,
onSelect() { onSelect() {
onSelectNone?.(); 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, checkStrictly,
} = mergedRowSelection.value; } = mergedRowSelection.value;
const { const { prefixCls, getRecordByKey, getRowKey, expandType, getPopupContainer } = configRef;
prefixCls,
getRecordByKey,
getRowKey,
expandType,
expandIconColumnIndex,
getPopupContainer,
} = configRef;
if (!rowSelectionRef.value) { 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 // Support selection
let cloneColumns = columns.slice();
const keySet = new Set(derivedSelectedKeySet.value); const keySet = new Set(derivedSelectedKeySet.value);
// Record key only need check with enabled // Record key only need check with enabled
@ -587,8 +611,62 @@ export default function useSelection<RecordType>(
return node; 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 = { const selectionColumn = {
fixed: mergedFixed,
width: selectionColWidth, width: selectionColWidth,
className: `${prefixCls.value}-selection-column`, className: `${prefixCls.value}-selection-column`,
title: mergedRowSelection.value.columnTitle || title, title: mergedRowSelection.value.columnTitle || title,
@ -598,15 +676,7 @@ export default function useSelection<RecordType>(
}, },
}; };
if (expandType.value === 'row' && columns.length && !expandIconColumnIndex.value) { return cloneColumns.map(col => (col === SELECTION_COLUMN ? selectionColumn : col));
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 [transformColumns, derivedSelectedKeySet]; return [transformColumns, derivedSelectedKeySet];

View File

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

View File

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

View File

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

View File

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

View File

@ -12,3 +12,5 @@ import '../../dropdown/style';
import '../../spin/style'; import '../../spin/style';
import '../../pagination/style'; import '../../pagination/style';
import '../../tooltip/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} & { .@{table-wrapepr-rtl-cls} & {
text-align: right; text-align: right;
} }
@ -73,7 +80,7 @@
// ============================ Sorter ============================ // ============================ Sorter ============================
&-column-sorter { &-column-sorter {
.@{table-wrapepr-rtl-cls} & { .@{table-wrapepr-rtl-cls} & {
margin-right: @padding-xs; margin-right: 4px;
margin-left: 0; margin-left: 0;
} }
} }
@ -93,10 +100,9 @@
} }
} }
&-filter-trigger-container { &-filter-trigger {
.@{table-wrapepr-rtl-cls} & { .@{table-wrapepr-rtl-cls} & {
right: auto; margin: -4px 4px -4px (-@table-padding-horizontal / 2);
left: 0;
} }
} }

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,9 @@ import VCResizeObserver from '../vc-resize-observer';
import { useProvideTable } from './context/TableContext'; import { useProvideTable } from './context/TableContext';
import { useProvideBody } from './context/BodyContext'; import { useProvideBody } from './context/BodyContext';
import { useProvideResize } from './context/ResizeContext'; 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 // Used for conditions cache
const EMPTY_DATA = []; const EMPTY_DATA = [];
@ -288,6 +290,17 @@ export default defineComponent<TableProps<DefaultRecordType>>({
emit('expandedRowsChange', newExpandedKeys); 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 componentWidth = ref(0);
const [columns, flattenColumns] = useColumns( const [columns, flattenColumns] = useColumns(
@ -444,8 +457,11 @@ export default defineComponent<TableProps<DefaultRecordType>>({
}; };
const triggerOnScroll = () => { const triggerOnScroll = () => {
if (scrollBodyRef.value) { if (horizonScroll.value && scrollBodyRef.value) {
onScroll({ currentTarget: scrollBodyRef.value }); onScroll({ currentTarget: scrollBodyRef.value });
} else {
setPingedLeft(false);
setPingedRight(false);
} }
}; };
let timtout; let timtout;
@ -473,7 +489,7 @@ export default defineComponent<TableProps<DefaultRecordType>>({
}); });
const [scrollbarSize, setScrollbarSize] = useState(0); const [scrollbarSize, setScrollbarSize] = useState(0);
useProvideSticky();
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
triggerOnScroll(); triggerOnScroll();
@ -554,10 +570,6 @@ export default defineComponent<TableProps<DefaultRecordType>>({
columns, columns,
flattenColumns, flattenColumns,
tableLayout: mergedTableLayout, tableLayout: mergedTableLayout,
componentWidth,
fixHeader,
fixColumn,
horizonScroll,
expandIcon: mergedExpandIcon, expandIcon: mergedExpandIcon,
expandableType, expandableType,
onTriggerExpand, onTriggerExpand,
@ -568,6 +580,13 @@ export default defineComponent<TableProps<DefaultRecordType>>({
onColumnResize, onColumnResize,
}); });
useProvideExpandedRow({
componentWidth,
fixHeader,
fixColumn,
horizonScroll,
});
// Body // Body
const bodyTable = () => ( const bodyTable = () => (
<Body <Body
@ -773,7 +792,7 @@ export default defineComponent<TableProps<DefaultRecordType>>({
</div> </div>
); );
} }
const ariaProps = getDataAndAriaProps(attrs); const ariaProps = pickAttrs(attrs, { aria: true, data: true });
const fullTable = () => ( const fullTable = () => (
<div <div
{...ariaProps} {...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>; columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[]; flattenColumns: readonly ColumnType<RecordType>[];
componentWidth: number;
tableLayout: TableLayout; tableLayout: TableLayout;
fixHeader: boolean;
fixColumn: boolean;
horizonScroll: boolean;
indentSize: number; indentSize: number;
expandableType: ExpandableType; 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, ColumnGroupType,
} from '../interface'; } from '../interface';
import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil'; import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil';
import { EXPAND_COLUMN } from '../constant';
function flatColumns<RecordType>(columns: ColumnsType<RecordType>): ColumnType<RecordType>[] { function flatColumns<RecordType>(columns: ColumnsType<RecordType>): ColumnType<RecordType>[] {
return columns.reduce((list, column) => { return columns.reduce((list, column) => {
@ -121,8 +122,38 @@ function useColumns<RecordType>(
// Add expand column // Add expand column
const withExpandColumns = computed<ColumnsType<RecordType>>(() => { const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
if (expandable.value) { if (expandable.value) {
const expandColIndex = expandIconColumnIndex.value || 0; let cloneColumns = baseColumns.value.slice();
const prevColumn = baseColumns.value[expandColIndex];
// >>> 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; let fixedColumn: FixedType | null;
if ((expandFixed.value === 'left' || expandFixed.value) && !expandIconColumnIndex.value) { if ((expandFixed.value === 'left' || expandFixed.value) && !expandIconColumnIndex.value) {
@ -140,9 +171,11 @@ function useColumns<RecordType>(
const expandIconValue = expandIcon.value; const expandIconValue = expandIcon.value;
const prefixClsValue = prefixCls.value; const prefixClsValue = prefixCls.value;
const expandRowByClickValue = expandRowByClick.value; const expandRowByClickValue = expandRowByClick.value;
// >>> Create expandable column
const expandColumn = { const expandColumn = {
[INTERNAL_COL_DEFINE]: { [INTERNAL_COL_DEFINE]: {
class: `${prefixCls.value}-expand-icon-col`, class: `${prefixCls.value}-expand-icon-col`,
columnType: 'EXPAND_COLUMN',
}, },
title: '', title: '',
fixed: fixedColumn, fixed: fixedColumn,
@ -168,14 +201,13 @@ function useColumns<RecordType>(
}, },
}; };
// Insert expand column in the target position return cloneColumns.map(col => (col === EXPAND_COLUMN ? expandColumn : col));
const cloneColumns = baseColumns.value.slice();
if (expandColIndex >= 0) {
cloneColumns.splice(expandColIndex, 0, expandColumn);
}
return cloneColumns;
} }
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(() => { const mergedColumns = computed(() => {

View File

@ -9,12 +9,14 @@ function flatRecord<T>(
childrenColumnName: string, childrenColumnName: string,
expandedKeys: Set<Key>, expandedKeys: Set<Key>,
getRowKey: GetRowKey<T>, getRowKey: GetRowKey<T>,
index: number,
) { ) {
const arr = []; const arr = [];
arr.push({ arr.push({
record, record,
indent, indent,
index,
}); });
const key = getRowKey(record); const key = getRowKey(record);
@ -30,6 +32,7 @@ function flatRecord<T>(
childrenColumnName, childrenColumnName,
expandedKeys, expandedKeys,
getRowKey, getRowKey,
i,
); );
arr.push(...tempArr); arr.push(...tempArr);
@ -56,27 +59,30 @@ export default function useFlattenRecords<T = unknown>(
expandedKeysRef: Ref<Set<Key>>, expandedKeysRef: Ref<Set<Key>>,
getRowKey: Ref<GetRowKey<T>>, 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 childrenColumnName = childrenColumnNameRef.value;
const expandedKeys = expandedKeysRef.value; const expandedKeys = expandedKeysRef.value;
const data = dataRef.value; const data = dataRef.value;
if (expandedKeys?.size) { if (expandedKeys?.size) {
const temp: { record: T; indent: number }[] = []; const temp: { record: T; indent: number; index: number }[] = [];
// collect flattened record // collect flattened record
for (let i = 0; i < data?.length; i += 1) { for (let i = 0; i < data?.length; i += 1) {
const record = data[i]; 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 temp;
} }
return data?.map(item => { return data?.map((item, index) => {
return { return {
record: item, record: item,
indent: 0, 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 Table from './Table';
import { FooterComponents as Summary, SummaryCell, SummaryRow } from './Footer'; import { FooterComponents as Summary, SummaryCell, SummaryRow } from './Footer';
import Column from './sugar/Column'; import Column from './sugar/Column';
import ColumnGroup from './sugar/ColumnGroup'; import ColumnGroup from './sugar/ColumnGroup';
import { INTERNAL_COL_DEFINE } from './utils/legacyUtil'; 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; export default Table;

View File

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

View File

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

View File

@ -9,34 +9,40 @@ export function getExpandableProps<RecordType>(
}, },
): ExpandableConfig<RecordType> { ): ExpandableConfig<RecordType> {
const { expandable, ...legacyExpandableConfig } = props; const { expandable, ...legacyExpandableConfig } = props;
let config: ExpandableConfig<RecordType>;
if (props.expandable !== undefined) { if (props.expandable !== undefined) {
return { config = {
...legacyExpandableConfig, ...legacyExpandableConfig,
...expandable, ...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 ( return config;
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;
} }
/** /**