refactor: table

pull/4639/head
tangjinzhou 2021-09-05 22:56:19 +08:00
parent de233c9c97
commit 456e3ae404
35 changed files with 3701 additions and 2780 deletions

View File

@ -1,4 +1,4 @@
import { defineComponent, inject, nextTick } from 'vue';
import { defineComponent, ExtractPropTypes, inject, nextTick } from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import VcCheckbox from '../vc-checkbox';
@ -9,11 +9,8 @@ import type { RadioChangeEvent } from '../radio/interface';
import type { EventHandler } from '../_util/EventInterface';
function noop() {}
export default defineComponent({
name: 'ACheckbox',
inheritAttrs: false,
__ANT_CHECKBOX: true,
props: {
export const checkboxProps = () => {
return {
prefixCls: PropTypes.string,
defaultChecked: PropTypes.looseBool,
checked: PropTypes.looseBool,
@ -27,7 +24,17 @@ export default defineComponent({
autofocus: PropTypes.looseBool,
onChange: PropTypes.func,
'onUpdate:checked': PropTypes.func,
},
skipGroup: PropTypes.looseBool,
};
};
export type CheckboxProps = Partial<ExtractPropTypes<ReturnType<typeof checkboxProps>>>;
export default defineComponent({
name: 'ACheckbox',
inheritAttrs: false,
__ANT_CHECKBOX: true,
props: checkboxProps(),
emits: ['change', 'update:checked'],
setup() {
return {
@ -38,6 +45,9 @@ export default defineComponent({
watch: {
value(value, prevValue) {
if (this.skipGroup) {
return;
}
nextTick(() => {
const { checkboxGroupContext: checkboxGroup = {} } = this;
if (checkboxGroup.registerValue && checkboxGroup.cancelValue) {
@ -85,7 +95,7 @@ export default defineComponent({
const props = getOptionProps(this);
const { checkboxGroupContext: checkboxGroup, $attrs } = this;
const children = getSlot(this);
const { indeterminate, prefixCls: customizePrefixCls, ...restProps } = props;
const { indeterminate, prefixCls: customizePrefixCls, skipGroup, ...restProps } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
const {
@ -101,7 +111,7 @@ export default defineComponent({
prefixCls,
...restAttrs,
};
if (checkboxGroup) {
if (checkboxGroup && !skipGroup) {
checkboxProps.onChange = (...args) => {
this.$emit('change', ...args);
checkboxGroup.toggleOption({ label: children, value: props.value });

View File

@ -1,6 +1,7 @@
import type { App, Plugin } from 'vue';
import Checkbox from './Checkbox';
import Checkbox, { checkboxProps } from './Checkbox';
import CheckboxGroup from './Group';
export type { CheckboxProps } from './Checkbox';
Checkbox.Group = CheckboxGroup;
@ -10,7 +11,7 @@ Checkbox.install = function (app: App) {
app.component(CheckboxGroup.name, CheckboxGroup);
return app;
};
export { CheckboxGroup };
export { CheckboxGroup, checkboxProps };
export default Checkbox as typeof Checkbox &
Plugin & {
readonly Group: typeof CheckboxGroup;

View File

@ -238,11 +238,6 @@ const TableRow = {
for (let i = 0; i < columns.length; i += 1) {
const column = columns[i];
warning(
column.onCellClick === undefined,
'column[onCellClick] is deprecated, please use column[customCell] instead.',
);
cells.push(
<TableCell
prefixCls={prefixCls}

View File

@ -9,6 +9,7 @@ import type { ValidateMessages } from '../form/interface';
import type { TransferLocale } from '../transfer';
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
import type { PaginationLocale } from '../pagination/Pagination';
import { TableLocale } from '../table/interface';
interface TransferLocaleForEmpty {
description: string;
@ -16,7 +17,7 @@ interface TransferLocaleForEmpty {
export interface Locale {
locale: string;
Pagination?: PaginationLocale;
Table?: Record<string, any>;
Table?: TableLocale;
Popconfirm?: Record<string, any>;
Upload?: Record<string, any>;
Form?: {

View File

@ -556,8 +556,8 @@
@table-header-bg: @background-color-light;
@table-header-color: @heading-color;
@table-header-sort-bg: @background-color-base;
@table-body-sort-bg: rgba(0, 0, 0, 0.01);
@table-row-hover-bg: @primary-1;
@table-body-sort-bg: #fafafa;
@table-row-hover-bg: @background-color-light;
@table-selected-row-color: inherit;
@table-selected-row-bg: @primary-1;
@table-body-selected-sort-bg: @table-selected-row-bg;
@ -565,15 +565,31 @@
@table-expanded-row-bg: #fbfbfb;
@table-padding-vertical: 16px;
@table-padding-horizontal: 16px;
@table-padding-vertical-md: (@table-padding-vertical * 3 / 4);
@table-padding-horizontal-md: (@table-padding-horizontal / 2);
@table-padding-vertical-sm: (@table-padding-vertical / 2);
@table-padding-horizontal-sm: (@table-padding-horizontal / 2);
@table-border-color: @border-color-split;
@table-border-radius-base: @border-radius-base;
@table-footer-bg: @background-color-light;
@table-footer-color: @heading-color;
@table-header-bg-sm: transparent;
@table-header-bg-sm: @table-header-bg;
@table-font-size: @font-size-base;
@table-font-size-md: @table-font-size;
@table-font-size-sm: @table-font-size;
@table-header-cell-split-color: rgba(0, 0, 0, 0.06);
// Sorter
// Legacy: `table-header-sort-active-bg` is used for hover not real active
@table-header-sort-active-bg: darken(@table-header-bg, 3%);
@table-header-sort-active-bg: rgba(0, 0, 0, 0.04);
// Filter
@table-header-filter-active-bg: darken(@table-header-sort-active-bg, 5%);
@table-header-filter-active-bg: rgba(0, 0, 0, 0.04);
@table-filter-btns-bg: inherit;
@table-filter-dropdown-bg: @component-background;
@table-expand-icon-bg: @component-background;
@table-selection-column-width: 32px;
// Sticky
@table-sticky-scroll-bar-bg: fade(#000, 35%);
@table-sticky-scroll-bar-radius: 4px;
// Tag
// --

View File

@ -1,9 +1,10 @@
import { defineComponent } from 'vue';
import { columnProps } from './interface';
import { ColumnType } from './interface';
export default defineComponent({
export type ColumnProps = ColumnType;
export default defineComponent<ColumnProps>({
name: 'ATableColumn',
props: columnProps,
slots: ['title', 'filterIcon'],
render() {
return null;
},

View File

@ -1,15 +1,9 @@
import { defineComponent } from 'vue';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { tuple } from '../_util/type';
import { ColumnGroupProps } from '../vc-table/sugar/ColumnGroup';
export default defineComponent({
export default defineComponent<ColumnGroupProps<any>>({
name: 'ATableColumnGroup',
props: {
fixed: withUndefined(
PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('left', 'right'))]),
),
title: PropTypes.any,
},
slots: ['title'],
__ANT_TABLE_COLUMN_GROUP: true,
render() {
return null;

View File

@ -0,0 +1,40 @@
import classNames from '../_util/classNames';
import { TableLocale } from './interface';
interface DefaultExpandIconProps<RecordType> {
prefixCls: string;
onExpand: (record: RecordType, e: MouseEvent) => void;
record: RecordType;
expanded: boolean;
expandable: boolean;
}
function renderExpandIcon(locale: TableLocale) {
return function expandIcon<RecordType>({
prefixCls,
onExpand,
record,
expanded,
expandable,
}: DefaultExpandIconProps<RecordType>) {
const iconPrefix = `${prefixCls}-row-expand-icon`;
return (
<button
type="button"
onClick={e => {
onExpand(record, e!);
e.stopPropagation();
}}
class={classNames(iconPrefix, {
[`${iconPrefix}-spaced`]: !expandable,
[`${iconPrefix}-expanded`]: expandable && expanded,
[`${iconPrefix}-collapsed`]: expandable && !expanded,
})}
aria-label={expanded ? locale.collapse : locale.expand}
/>
);
};
}
export default renderExpandIcon;

View File

@ -1,20 +0,0 @@
import type { FunctionalComponent } from 'vue';
export interface FilterDropdownMenuWrapperProps {
class?: string;
}
const FilterDropdownMenuWrapper: FunctionalComponent<FilterDropdownMenuWrapperProps> = (
props,
{ slots },
) => {
return (
<div class={props.class} onClick={e => e.stopPropagation()}>
{slots.default?.()}
</div>
);
};
FilterDropdownMenuWrapper.inheritAttrs = false;
export default FilterDropdownMenuWrapper;

View File

@ -1,42 +0,0 @@
import { computed, defineComponent } from 'vue';
import Checkbox from '../checkbox';
import Radio from '../radio';
import { SelectionBoxProps } from './interface';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps } from '../_util/props-util';
export default defineComponent({
name: 'SelectionBox',
mixins: [BaseMixin],
inheritAttrs: false,
props: SelectionBoxProps,
setup(props) {
return {
checked: computed(() => {
const { store, defaultSelection, rowIndex } = props;
let checked = false;
if (store.selectionDirty) {
checked = store.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked =
store.selectedRowKeys.indexOf(rowIndex) >= 0 || defaultSelection.indexOf(rowIndex) >= 0;
}
return checked;
}),
};
},
render() {
const { type, rowIndex, ...rest } = { ...getOptionProps(this), ...this.$attrs } as any;
const { checked } = this;
const checkboxProps = {
checked,
...rest,
};
if (type === 'radio') {
checkboxProps.value = rowIndex;
return <Radio {...checkboxProps} />;
}
return <Checkbox {...checkboxProps} />;
},
});

View File

@ -1,188 +0,0 @@
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import Checkbox from '../checkbox';
import Dropdown from '../dropdown';
import Menu from '../menu';
import classNames from '../_util/classNames';
import { SelectionCheckboxAllProps } from './interface';
import BaseMixin from '../_util/BaseMixin';
import { computed, defineComponent } from 'vue';
function checkSelection({
store,
getCheckboxPropsByItem,
getRecordKey,
data,
type,
byDefaultChecked,
}) {
return byDefaultChecked
? data[type]((item, i) => getCheckboxPropsByItem(item, i).defaultChecked)
: data[type]((item, i) => store.selectedRowKeys.indexOf(getRecordKey(item, i)) >= 0);
}
function getIndeterminateState(props) {
const { store, data } = props;
if (!data.length) {
return false;
}
const someCheckedNotByDefaultChecked =
checkSelection({
...props,
data,
type: 'some',
byDefaultChecked: false,
}) &&
!checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
const someCheckedByDefaultChecked =
checkSelection({
...props,
data,
type: 'some',
byDefaultChecked: true,
}) &&
!checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: true,
});
if (store.selectionDirty) {
return someCheckedNotByDefaultChecked;
}
return someCheckedNotByDefaultChecked || someCheckedByDefaultChecked;
}
function getCheckState(props) {
const { store, data } = props;
if (!data.length) {
return false;
}
if (store.selectionDirty) {
return checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
}
return (
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
}) ||
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: true,
})
);
}
export default defineComponent({
name: 'SelectionCheckboxAll',
mixins: [BaseMixin],
inheritAttrs: false,
props: SelectionCheckboxAllProps,
setup(props) {
return {
defaultSelections: [],
checked: computed(() => {
return getCheckState(props);
}),
indeterminate: computed(() => {
return getIndeterminateState(props);
}),
};
},
created() {
const { $props: props } = this;
this.defaultSelections = props.hideDefaultSelections
? []
: [
{
key: 'all',
text: props.locale.selectAll,
},
{
key: 'invert',
text: props.locale.selectInvert,
},
];
},
methods: {
handleSelectAllChange(e) {
const { checked } = e.target;
this.$emit('select', checked ? 'all' : 'removeAll', 0, null);
},
renderMenus(selections) {
return selections.map((selection, index) => {
return (
<Menu.Item key={selection.key || index}>
<div
onClick={() => {
this.$emit('select', selection.key, index, selection.onSelect);
}}
>
{selection.text}
</div>
</Menu.Item>
);
});
},
},
render() {
const { disabled, prefixCls, selections, getPopupContainer, checked, indeterminate } = this;
const selectionPrefixCls = `${prefixCls}-selection`;
let customSelections = null;
if (selections) {
const newSelections = Array.isArray(selections)
? this.defaultSelections.concat(selections)
: this.defaultSelections;
const menu = (
<Menu class={`${selectionPrefixCls}-menu`} selectedKeys={[]}>
{this.renderMenus(newSelections)}
</Menu>
);
customSelections =
newSelections.length > 0 ? (
<Dropdown getPopupContainer={getPopupContainer} overlay={menu}>
<div class={`${selectionPrefixCls}-down`}>
<DownOutlined />
</div>
</Dropdown>
) : null;
}
return (
<div class={selectionPrefixCls}>
<Checkbox
class={classNames({ [`${selectionPrefixCls}-select-all-custom`]: customSelections })}
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={this.handleSelectAllChange}
/>
{customSelections}
</div>
);
},
});

1730
components/table/Table.tsx Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
import PropTypes from '../_util/vue-types';
import { computed, defineComponent } from 'vue';
import { getSlot } from '../_util/props-util';
import omit from 'omit.js';
const BodyRowProps = {
store: PropTypes.object,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
prefixCls: PropTypes.string,
};
export default function createBodyRow(Component = 'tr') {
const BodyRow = defineComponent({
name: 'BodyRow',
inheritAttrs: false,
props: BodyRowProps,
setup(props) {
return {
selected: computed(() => props.store?.selectedRowKeys.indexOf(props.rowKey) >= 0),
};
},
render() {
const rowProps = omit({ ...this.$props, ...this.$attrs }, [
'prefixCls',
'rowKey',
'store',
'class',
]);
const className = {
[`${this.prefixCls}-row-selected`]: this.selected,
[this.$attrs.class as string]: !!this.$attrs.class,
};
return (
<Component class={className} {...rowProps}>
{getSlot(this)}
</Component>
);
},
});
return BodyRow;
}

View File

@ -1,290 +0,0 @@
import { reactive, defineComponent, nextTick, computed, watch } from 'vue';
import FilterFilled from '@ant-design/icons-vue/FilterFilled';
import Menu, { SubMenu, MenuItem } from '../menu';
import classNames from '../_util/classNames';
import shallowequal from '../_util/shallowequal';
import Dropdown from '../dropdown';
import Checkbox from '../checkbox';
import Radio from '../radio';
import FilterDropdownMenuWrapper from './FilterDropdownMenuWrapper';
import { FilterMenuProps } from './interface';
import { isValidElement } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { cloneElement } from '../_util/vnode';
import BaseMixin2 from '../_util/BaseMixin2';
import { generateValueMaps } from './util';
import type { Key } from '../_util/type';
function stopPropagation(e) {
e.stopPropagation();
}
export default defineComponent({
name: 'FilterMenu',
mixins: [BaseMixin2],
inheritAttrs: false,
props: initDefaultProps(FilterMenuProps, {
column: {},
}),
setup(props) {
const sSelectedKeys = computed(() => props.selectedKeys);
const sVisible = computed(() => {
return 'filterDropdownVisible' in props.column ? props.column.filterDropdownVisible : false;
});
const sValueKeys = computed(() => generateValueMaps(props.column.filters));
const state = reactive({
neverShown: false,
sSelectedKeys: sSelectedKeys.value,
sKeyPathOfSelectedItem: {}, //
sVisible: sVisible.value,
sValueKeys: sValueKeys.value,
});
watch(sSelectedKeys, () => {
state.sSelectedKeys = sSelectedKeys.value;
});
watch(sVisible, () => {
state.sVisible = sVisible.value;
});
watch(sValueKeys, () => {
state.sValueKeys = sValueKeys.value;
});
// watchEffect(
// () => {
// const { column } = nextProps;
// if (!shallowequal(preProps.selectedKeys, nextProps.selectedKeys)) {
// state.sSelectedKeys = nextProps.selectedKeys;
// }
// if (!shallowequal((preProps.column || {}).filters, (nextProps.column || {}).filters)) {
// state.sValueKeys = generateValueMaps(nextProps.column.filters);
// }
// if ('filterDropdownVisible' in column) {
// state.sVisible = column.filterDropdownVisible;
// }
// preProps = { ...nextProps };
// },
// { flush: 'sync' },
// );
return state;
},
methods: {
getDropdownVisible() {
return !!this.sVisible;
},
setSelectedKeys({ selectedKeys }) {
this.setState({ sSelectedKeys: selectedKeys });
},
setVisible(visible: boolean) {
const { column } = this;
if (!('filterDropdownVisible' in column)) {
this.setState({ sVisible: visible });
}
if (column.onFilterDropdownVisibleChange) {
column.onFilterDropdownVisibleChange(visible);
}
},
handleClearFilters() {
this.setState(
{
sSelectedKeys: [],
},
this.handleConfirm,
);
},
handleConfirm() {
this.setVisible(false);
// Call `setSelectedKeys` & `confirm` in the same time will make filter data not up to date
// https://github.com/ant-design/ant-design/issues/12284
(this as any).$forceUpdate();
nextTick(this.confirmFilter2);
},
onVisibleChange(visible: boolean) {
this.setVisible(visible);
const { column } = this.$props;
// https://github.com/ant-design/ant-design/issues/17833
if (!visible && !(column.filterDropdown instanceof Function)) {
this.confirmFilter2();
}
},
handleMenuItemClick(info: { keyPath: Key[]; key: Key }) {
const { sSelectedKeys: selectedKeys } = this;
if (!info.keyPath || info.keyPath.length <= 1) {
return;
}
const { sKeyPathOfSelectedItem: keyPathOfSelectedItem } = this;
if (selectedKeys && selectedKeys.indexOf(info.key) >= 0) {
// deselect SubMenu child
delete keyPathOfSelectedItem[info.key];
} else {
// select SubMenu child
keyPathOfSelectedItem[info.key] = info.keyPath;
}
this.setState({ sKeyPathOfSelectedItem: keyPathOfSelectedItem });
},
hasSubMenu() {
const {
column: { filters = [] },
} = this;
return filters.some(item => !!(item.children && item.children.length > 0));
},
confirmFilter2() {
const { column, selectedKeys: propSelectedKeys, confirmFilter } = this.$props;
const { sSelectedKeys: selectedKeys, sValueKeys: valueKeys } = this;
const { filterDropdown } = column;
if (!shallowequal(selectedKeys, propSelectedKeys)) {
confirmFilter(
column,
filterDropdown
? selectedKeys
: selectedKeys.map((key: any) => valueKeys[key]).filter(key => key !== undefined),
);
}
},
renderMenus(items) {
const { dropdownPrefixCls, prefixCls } = this.$props;
return items.map(item => {
if (item.children && item.children.length > 0) {
const { sKeyPathOfSelectedItem } = this;
const containSelected = Object.keys(sKeyPathOfSelectedItem).some(
key => sKeyPathOfSelectedItem[key].indexOf(item.value) >= 0,
);
const subMenuCls = classNames(`${prefixCls}-dropdown-submenu`, {
[`${dropdownPrefixCls}-submenu-contain-selected`]: containSelected,
});
return (
<SubMenu title={item.text} popupClassName={subMenuCls} key={item.value}>
{this.renderMenus(item.children)}
</SubMenu>
);
}
return this.renderMenuItem(item);
});
},
renderFilterIcon() {
const { column, locale, prefixCls, selectedKeys } = this;
const filtered = selectedKeys && selectedKeys.length > 0;
let filterIcon = column.filterIcon;
if (typeof filterIcon === 'function') {
filterIcon = filterIcon({ filtered, column });
}
const dropdownIconClass = classNames({
[`${prefixCls}-selected`]: 'filtered' in column ? column.filtered : filtered,
[`${prefixCls}-open`]: this.getDropdownVisible(),
});
if (!filterIcon) {
return (
<FilterFilled
title={locale.filterTitle}
class={dropdownIconClass}
onClick={stopPropagation}
/>
);
}
if (filterIcon.length === 1 && isValidElement(filterIcon[0])) {
return cloneElement(filterIcon[0], {
title: filterIcon.props?.title || locale.filterTitle,
onClick: stopPropagation,
class: classNames(`${prefixCls}-icon`, dropdownIconClass, filterIcon.props?.class),
});
}
return (
<span class={classNames(`${prefixCls}-icon`, dropdownIconClass)} onClick={stopPropagation}>
{filterIcon}
</span>
);
},
renderMenuItem(item) {
const { column } = this;
const { sSelectedKeys: selectedKeys } = this;
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
const input = multiple ? (
<Checkbox checked={selectedKeys && selectedKeys.indexOf(item.value) >= 0} />
) : (
<Radio checked={selectedKeys && selectedKeys.indexOf(item.value) >= 0} />
);
return (
<MenuItem key={item.value}>
{input}
<span>{item.text}</span>
</MenuItem>
);
},
},
render() {
const { sSelectedKeys: originSelectedKeys } = this as any;
const { column, locale, prefixCls, dropdownPrefixCls, getPopupContainer } = this;
// default multiple selection in filter dropdown
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
const dropdownMenuClass = classNames({
[`${dropdownPrefixCls}-menu-without-submenu`]: !this.hasSubMenu(),
});
let { filterDropdown } = column;
if (filterDropdown instanceof Function) {
filterDropdown = filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: selectedKeys => this.setSelectedKeys({ selectedKeys }),
selectedKeys: originSelectedKeys,
confirm: this.handleConfirm,
clearFilters: this.handleClearFilters,
filters: column.filters,
visible: this.getDropdownVisible(),
column,
});
}
const menus = filterDropdown ? (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
{filterDropdown}
</FilterDropdownMenuWrapper>
) : (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
<Menu
multiple={multiple}
onClick={this.handleMenuItemClick}
prefixCls={`${dropdownPrefixCls}-menu`}
class={dropdownMenuClass}
onSelect={this.setSelectedKeys}
onDeselect={this.setSelectedKeys}
selectedKeys={originSelectedKeys}
getPopupContainer={getPopupContainer}
>
{this.renderMenus(column.filters)}
</Menu>
<div class={`${prefixCls}-dropdown-btns`}>
<a class={`${prefixCls}-dropdown-link confirm`} onClick={this.handleConfirm}>
{locale.filterConfirm}
</a>
<a class={`${prefixCls}-dropdown-link clear`} onClick={this.handleClearFilters}>
{locale.filterReset}
</a>
</div>
</FilterDropdownMenuWrapper>
);
return (
<Dropdown
trigger={['click']}
placement="bottomRight"
visible={this.getDropdownVisible()}
onVisibleChange={this.onVisibleChange}
getPopupContainer={getPopupContainer}
forceRender
overlay={menus}
>
{this.renderFilterIcon()}
</Dropdown>
);
},
});

View File

@ -0,0 +1,326 @@
import isEqual from 'lodash-es/isEqual';
import FilterFilled from '@ant-design/icons-vue/FilterFilled';
import Button from '../../../button';
import Menu from '../../../menu';
import Checkbox from '../../../checkbox';
import Radio from '../../../radio';
import Dropdown from '../../../dropdown';
import Empty from '../../../empty';
import { ColumnType, ColumnFilterItem, Key, TableLocale, GetPopupContainer } from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper';
import { FilterState } from '.';
import { computed, defineComponent, onBeforeUnmount, ref } from 'vue';
import classNames from 'ant-design-vue/es/_util/classNames';
import useConfigInject from 'ant-design-vue/es/_util/hooks/useConfigInject';
const { SubMenu, Item: MenuItem } = Menu;
function hasSubMenu(filters: ColumnFilterItem[]) {
return filters.some(({ children }) => children && children.length > 0);
}
function renderFilterItems({
filters,
prefixCls,
filteredKeys,
filterMultiple,
locale,
}: {
filters: ColumnFilterItem[];
prefixCls: string;
filteredKeys: Key[];
filterMultiple: boolean;
locale: TableLocale;
}) {
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);
if (filter.children) {
return (
<SubMenu
key={key || index}
title={filter.text}
popupClassName={`${prefixCls}-dropdown-submenu`}
>
{renderFilterItems({
filters: filter.children,
prefixCls,
filteredKeys,
filterMultiple,
locale,
})}
</SubMenu>
);
}
const Component = filterMultiple ? Checkbox : Radio;
return (
<MenuItem key={filter.value !== undefined ? key : index}>
<Component checked={filteredKeys.includes(key)} />
<span>{filter.text}</span>
</MenuItem>
);
});
}
export interface FilterDropdownProps<RecordType> {
tablePrefixCls: string;
prefixCls: string;
dropdownPrefixCls: string;
column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>;
filterMultiple: boolean;
columnKey: Key;
triggerFilter: (filterState: FilterState<RecordType>) => void;
locale: TableLocale;
getPopupContainer?: GetPopupContainer;
}
export default defineComponent<FilterDropdownProps<any>>({
name: 'FilterDropdown',
props: [
'tablePrefixCls',
'prefixCls',
'dropdownPrefixCls',
'column',
'filterState',
'filterMultiple',
'columnKey',
'triggerFilter',
'locale',
'getPopupContainer',
] as any,
setup(props, { slots }) {
const filterDropdownVisible = computed(() => props.column.filterDropdownVisible);
const visible = ref(false);
const filtered = computed(
() =>
!!(
props.filterState &&
(props.filterState.filteredKeys?.length || props.filterState.forceFiltered)
),
);
const triggerVisible = (newVisible: boolean) => {
visible.value = newVisible;
props.column.onFilterDropdownVisibleChange?.(newVisible);
};
const mergedVisible = computed(() =>
typeof filterDropdownVisible.value === 'boolean'
? filterDropdownVisible.value
: visible.value,
);
const filteredKeys = ref([]);
const mergedFilteredKeys = computed(
() => props.filterState?.filteredKeys || filteredKeys.value || [],
);
const onSelectKeys = ({ selectedKeys }: { selectedKeys?: Key[] }) => {
filteredKeys.value = selectedKeys;
};
const openKeys = ref([]);
const openRef = ref();
const onOpenChange = (keys: string[]) => {
openRef.value = window.setTimeout(() => {
openKeys.value = keys;
});
};
const onMenuClick = () => {
window.clearTimeout(openRef.value);
};
onBeforeUnmount(() => {
window.clearTimeout(openRef.value);
});
// ======================= Submit ========================
const internalTriggerFilter = (keys: Key[] | undefined | null) => {
const { column, columnKey, filterState } = props;
const mergedKeys = keys && keys.length ? keys : null;
if (mergedKeys === null && (!filterState || !filterState.filteredKeys)) {
return null;
}
if (isEqual(mergedKeys, filterState?.filteredKeys)) {
return null;
}
props.triggerFilter({
column,
key: columnKey,
filteredKeys: mergedKeys,
});
};
const onConfirm = () => {
triggerVisible(false);
internalTriggerFilter(mergedFilteredKeys.value);
};
const onReset = () => {
filteredKeys.value = [];
triggerVisible(false);
internalTriggerFilter([]);
};
const doFilter = ({ closeDropdown } = { closeDropdown: true }) => {
if (closeDropdown) {
triggerVisible(false);
}
internalTriggerFilter(mergedFilteredKeys.value);
};
const onVisibleChange = (newVisible: boolean) => {
triggerVisible(newVisible);
// Default will filter when closed
if (!newVisible && !props.column.filterDropdown) {
onConfirm();
}
};
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 || []),
});
let dropdownContent;
if (typeof column.filterDropdown === 'function') {
dropdownContent = column.filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: (selectedKeys: Key[]) => onSelectKeys({ selectedKeys }),
selectedKeys: mergedFilteredKeys.value,
confirm: doFilter,
clearFilters: onReset,
filters: column.filters,
visible: mergedVisible.value,
});
} else if (column.filterDropdown) {
dropdownContent = column.filterDropdown;
} else {
const selectedKeys = mergedFilteredKeys.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: mergedFilteredKeys.value,
filterMultiple,
locale,
}),
}}
></Menu>
<div class={`${prefixCls}-dropdown-btns`}>
<Button
type="link"
size="small"
disabled={selectedKeys.length === 0}
onClick={onReset}
>
{locale.filterReset}
</Button>
<Button type="primary" size="small" onClick={onConfirm}>
{locale.filterConfirm}
</Button>
</div>
</>
);
}
const menu = (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
{dropdownContent}
</FilterDropdownMenuWrapper>
);
let filterIcon;
if (typeof column.filterIcon === 'function') {
filterIcon = column.filterIcon(filtered.value);
} else if (column.filterIcon) {
filterIcon = column.filterIcon;
} else {
filterIcon = <FilterFilled />;
}
return (
<div class={`${prefixCls}-column`}>
<span class={`${tablePrefixCls}-column-title`}>{slots.defalut?.()}</span>
<Dropdown
overlay={menu}
trigger={['click']}
visible={mergedVisible.value}
onVisibleChange={onVisibleChange}
getPopupContainer={getPopupContainer}
placement={direction.value === 'rtl' ? 'bottomLeft' : 'bottomRight'}
>
<span
role="button"
tabindex={-1}
class={classNames(`${prefixCls}-trigger`, {
active: filtered.value,
})}
onClick={e => {
e.stopPropagation();
}}
>
{filterIcon}
</span>
</Dropdown>
</div>
);
};
},
});

View File

@ -0,0 +1,5 @@
const FilterDropdownMenuWrapper = (_props, { slots }) => (
<div onClick={e => e.stopPropagation()}>{slots.default?.()}</div>
);
export default FilterDropdownMenuWrapper;

View File

@ -0,0 +1,258 @@
import { DefaultRecordType } from 'ant-design-vue/es/vc-table/interface';
import devWarning from 'ant-design-vue/es/vc-util/devWarning';
import useState from 'ant-design-vue/es/_util/hooks/useState';
import { computed, Ref } from 'vue';
import {
TransformColumns,
ColumnsType,
ColumnType,
ColumnTitleProps,
Key,
TableLocale,
FilterValue,
FilterKey,
GetPopupContainer,
ColumnFilterItem,
} from '../../interface';
import { getColumnPos, renderColumnTitle, getColumnKey } from '../../util';
import FilterDropdown from './FilterDropdown';
export interface FilterState<RecordType = DefaultRecordType> {
column: ColumnType<RecordType>;
key: Key;
filteredKeys?: FilterKey;
forceFiltered?: boolean;
}
function collectFilterStates<RecordType>(
columns: ColumnsType<RecordType>,
init: boolean,
pos?: string,
): FilterState<RecordType>[] {
let filterStates: FilterState<RecordType>[] = [];
(columns || []).forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
if ('children' in column) {
filterStates = [...filterStates, ...collectFilterStates(column.children, init, columnPos)];
} else if (column.filters || 'filterDropdown' in column || 'onFilter' in column) {
if ('filteredValue' in column) {
// Controlled
let filteredValues = column.filteredValue;
if (!('filterDropdown' in column)) {
filteredValues = filteredValues?.map(String) ?? filteredValues;
}
filterStates.push({
column,
key: getColumnKey(column, columnPos),
filteredKeys: filteredValues as FilterKey,
forceFiltered: column.filtered,
});
} else {
// Uncontrolled
filterStates.push({
column,
key: getColumnKey(column, columnPos),
filteredKeys: (init && column.defaultFilteredValue
? column.defaultFilteredValue!
: undefined) as FilterKey,
forceFiltered: column.filtered,
});
}
}
});
return filterStates;
}
function injectFilter<RecordType>(
prefixCls: string,
dropdownPrefixCls: string,
columns: ColumnsType<RecordType>,
filterStates: FilterState<RecordType>[],
triggerFilter: (filterState: FilterState<RecordType>) => void,
getPopupContainer: GetPopupContainer | undefined,
locale: TableLocale,
pos?: string,
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
const { filterMultiple = true } = column as ColumnType<RecordType>;
let newColumn: ColumnsType<RecordType>[number] = column;
if (newColumn.filters || newColumn.filterDropdown) {
const columnKey = getColumnKey(newColumn, columnPos);
const filterState = filterStates.find(({ key }) => columnKey === key);
newColumn = {
...newColumn,
title: (renderProps: ColumnTitleProps<RecordType>) => (
<FilterDropdown
tablePrefixCls={prefixCls}
prefixCls={`${prefixCls}-filter`}
dropdownPrefixCls={dropdownPrefixCls}
column={newColumn}
columnKey={columnKey}
filterState={filterState}
filterMultiple={filterMultiple}
triggerFilter={triggerFilter}
locale={locale}
getPopupContainer={getPopupContainer}
>
{renderColumnTitle(column.title, renderProps)}
</FilterDropdown>
),
};
}
if ('children' in newColumn) {
newColumn = {
...newColumn,
children: injectFilter(
prefixCls,
dropdownPrefixCls,
newColumn.children,
filterStates,
triggerFilter,
getPopupContainer,
locale,
columnPos,
),
};
}
return newColumn;
});
}
function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: FilterValue = [];
(filters || []).forEach(({ value, children }) => {
keys.push(value);
if (children) {
keys = [...keys, ...flattenKeys(children)];
}
});
return keys;
}
function generateFilterInfo<RecordType>(filterStates: FilterState<RecordType>[]) {
const currentFilters: Record<string, FilterValue | null> = {};
filterStates.forEach(({ key, filteredKeys, column }) => {
const { filters, filterDropdown } = column;
if (filterDropdown) {
currentFilters[key] = filteredKeys || null;
} else if (Array.isArray(filteredKeys)) {
const keys = flattenKeys(filters);
currentFilters[key] = keys.filter(originKey => filteredKeys.includes(String(originKey)));
} else {
currentFilters[key] = null;
}
});
return currentFilters;
}
export function getFilterData<RecordType>(
data: RecordType[],
filterStates: FilterState<RecordType>[],
) {
return filterStates.reduce((currentData, filterState) => {
const {
column: { onFilter, filters },
filteredKeys,
} = filterState;
if (onFilter && filteredKeys && filteredKeys.length) {
return currentData.filter(record =>
filteredKeys.some(key => {
const keys = flattenKeys(filters);
const keyIndex = keys.findIndex(k => String(k) === String(key));
const realKey = keyIndex !== -1 ? keys[keyIndex] : key;
return onFilter(realKey, record);
}),
);
}
return currentData;
}, data);
}
interface FilterConfig<RecordType> {
prefixCls: Ref<string>;
dropdownPrefixCls: Ref<string>;
mergedColumns: Ref<ColumnsType<RecordType>>;
locale: Ref<TableLocale>;
onFilterChange: (
filters: Record<string, FilterValue | null>,
filterStates: FilterState<RecordType>[],
) => void;
getPopupContainer?: Ref<GetPopupContainer>;
}
function useFilter<RecordType>({
prefixCls,
dropdownPrefixCls,
mergedColumns,
locale,
onFilterChange,
getPopupContainer,
}: FilterConfig<RecordType>): [
TransformColumns<RecordType>,
Ref<FilterState<RecordType>[]>,
Ref<Record<string, FilterValue | null>>,
] {
const [filterStates, setFilterStates] = useState<FilterState<RecordType>[]>(
collectFilterStates(mergedColumns.value, true),
);
const mergedFilterStates = computed(() => {
const collectedStates = collectFilterStates(mergedColumns.value, false);
const filteredKeysIsNotControlled = collectedStates.every(
({ filteredKeys }) => filteredKeys === undefined,
);
// Return if not controlled
if (filteredKeysIsNotControlled) {
return filterStates.value;
}
const filteredKeysIsAllControlled = collectedStates.every(
({ filteredKeys }) => filteredKeys !== undefined,
);
devWarning(
filteredKeysIsNotControlled || filteredKeysIsAllControlled,
'Table',
'`FilteredKeys` should all be controlled or not controlled.',
);
return collectedStates;
});
const filters = computed(() => generateFilterInfo(mergedFilterStates.value));
const triggerFilter = (filterState: FilterState<RecordType>) => {
const newFilterStates = mergedFilterStates.value.filter(({ key }) => key !== filterState.key);
newFilterStates.push(filterState);
setFilterStates(newFilterStates);
onFilterChange(generateFilterInfo(newFilterStates), newFilterStates);
};
const transformColumns = (innerColumns: ColumnsType<RecordType>) => {
return injectFilter(
prefixCls.value,
dropdownPrefixCls.value,
innerColumns,
mergedFilterStates.value,
triggerFilter,
getPopupContainer.value,
locale.value,
);
};
return [transformColumns, mergedFilterStates, filters];
}
export default useFilter;

View File

@ -0,0 +1,51 @@
import type { Ref } from 'vue';
import { watch } from 'vue';
import { ref } from 'vue';
import type { Key, GetRowKey } from '../interface';
interface MapCache<RecordType> {
kvMap?: Map<Key, RecordType>;
}
export default function useLazyKVMap<RecordType>(
dataRef: Ref<readonly RecordType[]>,
childrenColumnNameRef: Ref<string>,
getRowKeyRef: Ref<GetRowKey<RecordType>>,
) {
const mapCacheRef = ref<MapCache<RecordType>>({});
watch(
[dataRef, childrenColumnNameRef, getRowKeyRef],
() => {
const kvMap = new Map<Key, RecordType>();
const getRowKey = getRowKeyRef.value;
const childrenColumnName = childrenColumnNameRef.value;
/* eslint-disable no-inner-declarations */
function dig(records: readonly RecordType[]) {
records.forEach((record, index) => {
const rowKey = getRowKey(record, index);
kvMap.set(rowKey, record);
if (record && typeof record === 'object' && childrenColumnName in record) {
dig((record as any)[childrenColumnName] || []);
}
});
}
/* eslint-enable */
dig(dataRef.value);
mapCacheRef.value = {
kvMap,
};
},
{
deep: false,
},
);
function getRecordByKey(key: Key): RecordType {
return mapCacheRef.value.kvMap!.get(key)!;
}
return [getRecordByKey];
}

View File

@ -0,0 +1,107 @@
import useState from 'ant-design-vue/es/_util/hooks/useState';
import type { Ref } from 'vue';
import { computed } from 'vue';
import type { PaginationProps } from '../../pagination';
import type { TablePaginationConfig } from '../interface';
export const DEFAULT_PAGE_SIZE = 10;
export function getPaginationParam(
pagination: TablePaginationConfig | boolean | undefined,
mergedPagination: TablePaginationConfig,
) {
const param: any = {
current: mergedPagination.current,
pageSize: mergedPagination.pageSize,
};
const paginationObj = pagination && typeof pagination === 'object' ? pagination : {};
Object.keys(paginationObj).forEach(pageProp => {
const value = (mergedPagination as any)[pageProp];
if (typeof value !== 'function') {
param[pageProp] = value;
}
});
return param;
}
function extendsObject<T extends Object>(...list: T[]) {
const result: T = {} as T;
list.forEach(obj => {
if (obj) {
Object.keys(obj).forEach(key => {
const val = (obj as any)[key];
if (val !== undefined) {
(result as any)[key] = val;
}
});
}
});
return result;
}
export default function usePagination(
totalRef: Ref<number>,
paginationRef: Ref<TablePaginationConfig | false | undefined>,
onChange: (current: number, pageSize: number) => void,
): [Ref<TablePaginationConfig>, () => void] {
const pagination = computed(() =>
paginationRef.value && typeof paginationRef.value === 'object' ? paginationRef.value : {},
);
const paginationTotal = computed(() => pagination.value.total || 0);
const [innerPagination, setInnerPagination] = useState<{
current?: number;
pageSize?: number;
}>(() => ({
current: 'defaultCurrent' in pagination.value ? pagination.value.defaultCurrent : 1,
pageSize:
'defaultPageSize' in pagination.value ? pagination.value.defaultPageSize : DEFAULT_PAGE_SIZE,
}));
// ============ Basic Pagination Config ============
const mergedPagination = computed(() =>
extendsObject<Partial<TablePaginationConfig>>(innerPagination.value, pagination.value, {
total: paginationTotal.value > 0 ? paginationTotal.value : totalRef.value,
}),
);
// Reset `current` if data length or pageSize changed
const maxPage = Math.ceil(
(paginationTotal.value || totalRef.value) / mergedPagination.value.pageSize!,
);
if (mergedPagination.value.current! > maxPage) {
// Prevent a maximum page count of 0
mergedPagination.value.current = maxPage || 1;
}
const refreshPagination = (current = 1, pageSize?: number) => {
setInnerPagination({
current,
pageSize: pageSize || mergedPagination.value.pageSize,
});
};
const onInternalChange: PaginationProps['onChange'] = (current, pageSize) => {
if (pagination.value) {
pagination.value.onChange?.(current, pageSize);
}
refreshPagination(current, pageSize);
onChange(current, pageSize || mergedPagination.value.pageSize);
};
if (pagination.value === false) {
return [computed(() => ({})), () => {}];
}
return [
computed(() => ({
...mergedPagination.value,
onChange: onInternalChange,
})),
refreshPagination,
];
}

View File

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

View File

@ -0,0 +1,426 @@
import CaretDownOutlined from '@ant-design/icons-vue/CaretDownOutlined';
import CaretUpOutlined from '@ant-design/icons-vue/CaretUpOutlined';
import {
TransformColumns,
ColumnsType,
Key,
ColumnType,
SortOrder,
CompareFn,
ColumnTitleProps,
SorterResult,
ColumnGroupType,
TableLocale,
} from '../interface';
import Tooltip, { TooltipProps } from '../../tooltip';
import { getColumnKey, getColumnPos, renderColumnTitle } from '../util';
import classNames from 'ant-design-vue/es/_util/classNames';
import { computed, Ref } from 'vue';
import useState from 'ant-design-vue/es/_util/hooks/useState';
import { DefaultRecordType } from 'ant-design-vue/es/vc-table/interface';
const ASCEND = 'ascend';
const DESCEND = 'descend';
function getMultiplePriority<RecordType>(column: ColumnType<RecordType>): number | false {
if (typeof column.sorter === 'object' && typeof column.sorter.multiple === 'number') {
return column.sorter.multiple;
}
return false;
}
function getSortFunction<RecordType>(
sorter: ColumnType<RecordType>['sorter'],
): CompareFn<RecordType> | false {
if (typeof sorter === 'function') {
return sorter;
}
if (sorter && typeof sorter === 'object' && sorter.compare) {
return sorter.compare;
}
return false;
}
function nextSortDirection(sortDirections: SortOrder[], current: SortOrder | null) {
if (!current) {
return sortDirections[0];
}
return sortDirections[sortDirections.indexOf(current) + 1];
}
export interface SortState<RecordType = DefaultRecordType> {
column: ColumnType<RecordType>;
key: Key;
sortOrder: SortOrder | null;
multiplePriority: number | false;
}
function collectSortStates<RecordType>(
columns: ColumnsType<RecordType>,
init: boolean,
pos?: string,
): SortState<RecordType>[] {
let sortStates: SortState<RecordType>[] = [];
function pushState(column: ColumnsType<RecordType>[number], columnPos: string) {
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.sortOrder!,
});
}
(columns || []).forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
if ((column as ColumnGroupType<RecordType>).children) {
if ('sortOrder' in column) {
// Controlled
pushState(column, columnPos);
}
sortStates = [
...sortStates,
...collectSortStates((column as ColumnGroupType<RecordType>).children, init, columnPos),
];
} else if (column.sorter) {
if ('sortOrder' in column) {
// Controlled
pushState(column, columnPos);
} else if (init && column.defaultSortOrder) {
// Default sorter
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.defaultSortOrder!,
});
}
}
});
return sortStates;
}
function injectSorter<RecordType>(
prefixCls: string,
columns: ColumnsType<RecordType>,
sorterSates: SortState<RecordType>[],
triggerSorter: (sorterSates: SortState<RecordType>) => void,
defaultSortDirections: SortOrder[],
tableLocale?: TableLocale,
tableShowSorterTooltip?: boolean | TooltipProps,
pos?: string,
): ColumnsType<RecordType> {
return (columns || []).map((column, index) => {
const columnPos = getColumnPos(index, pos);
let newColumn: ColumnsType<RecordType>[number] = column;
if (newColumn.sorter) {
const sortDirections: SortOrder[] = newColumn.sortDirections || defaultSortDirections;
const showSorterTooltip =
newColumn.showSorterTooltip === undefined
? tableShowSorterTooltip
: newColumn.showSorterTooltip;
const columnKey = getColumnKey(newColumn, columnPos);
const sorterState = sorterSates.find(({ key }) => key === columnKey);
const sorterOrder = sorterState ? sorterState.sortOrder : null;
const nextSortOrder = nextSortDirection(sortDirections, sorterOrder);
const upNode = sortDirections.includes(ASCEND) && (
<CaretUpOutlined
class={classNames(`${prefixCls}-column-sorter-up`, {
active: sorterOrder === ASCEND,
})}
/>
);
const downNode = sortDirections.includes(DESCEND) && (
<CaretDownOutlined
class={classNames(`${prefixCls}-column-sorter-down`, {
active: sorterOrder === DESCEND,
})}
/>
);
const { cancelSort, triggerAsc, triggerDesc } = tableLocale || {};
let sortTip: string | undefined = cancelSort;
if (nextSortOrder === DESCEND) {
sortTip = triggerDesc;
} else if (nextSortOrder === ASCEND) {
sortTip = triggerAsc;
}
const tooltipProps: TooltipProps =
typeof showSorterTooltip === 'object' ? showSorterTooltip : { title: sortTip };
newColumn = {
...newColumn,
className: classNames(newColumn.className, { [`${prefixCls}-column-sort`]: sorterOrder }),
title: (renderProps: ColumnTitleProps<RecordType>) => {
const renderSortTitle = (
<div class={`${prefixCls}-column-sorters`}>
<span class={`${prefixCls}-column-title`}>
{renderColumnTitle(column.title, renderProps)}
</span>
<span
class={classNames(`${prefixCls}-column-sorter`, {
[`${prefixCls}-column-sorter-full`]: !!(upNode && downNode),
})}
>
<span class={`${prefixCls}-column-sorter-inner`}>
{upNode}
{downNode}
</span>
</span>
</div>
);
return showSorterTooltip ? (
<Tooltip {...tooltipProps}>{renderSortTitle}</Tooltip>
) : (
renderSortTitle
);
},
customHeaderCell: col => {
const cell = (column.customHeaderCell && column.customHeaderCell(col)) || {};
const originOnClick = cell.onClick;
cell.onClick = (event: MouseEvent) => {
triggerSorter({
column,
key: columnKey,
sortOrder: nextSortOrder,
multiplePriority: getMultiplePriority(column),
});
if (originOnClick) {
originOnClick(event);
}
};
cell.class = classNames(cell.class, `${prefixCls}-column-has-sorters`);
return cell;
},
};
}
if ('children' in newColumn) {
newColumn = {
...newColumn,
children: injectSorter(
prefixCls,
newColumn.children,
sorterSates,
triggerSorter,
defaultSortDirections,
tableLocale,
tableShowSorterTooltip,
columnPos,
),
};
}
return newColumn;
});
}
function stateToInfo<RecordType>(sorterStates: SortState<RecordType>) {
const { column, sortOrder } = sorterStates;
return { column, order: sortOrder, field: column.dataIndex, columnKey: column.key };
}
function generateSorterInfo<RecordType>(
sorterStates: SortState<RecordType>[],
): SorterResult<RecordType> | SorterResult<RecordType>[] {
const list = sorterStates.filter(({ sortOrder }) => sortOrder).map(stateToInfo);
// =========== Legacy compatible support ===========
// https://github.com/ant-design/ant-design/pull/19226
if (list.length === 0 && sorterStates.length) {
return {
...stateToInfo(sorterStates[sorterStates.length - 1]),
column: undefined,
};
}
if (list.length <= 1) {
return list[0] || {};
}
return list;
}
export function getSortData<RecordType>(
data: readonly RecordType[],
sortStates: SortState<RecordType>[],
childrenColumnName: string,
): RecordType[] {
const innerSorterStates = sortStates
.slice()
.sort((a, b) => (b.multiplePriority as number) - (a.multiplePriority as number));
const cloneData = data.slice();
const runningSorters = innerSorterStates.filter(
({ column: { sorter }, sortOrder }) => getSortFunction(sorter) && sortOrder,
);
// Skip if no sorter needed
if (!runningSorters.length) {
return cloneData;
}
return cloneData
.sort((record1, record2) => {
for (let i = 0; i < runningSorters.length; i += 1) {
const sorterState = runningSorters[i];
const {
column: { sorter },
sortOrder,
} = sorterState;
const compareFn = getSortFunction(sorter);
if (compareFn && sortOrder) {
const compareResult = compareFn(record1, record2, sortOrder);
if (compareResult !== 0) {
return sortOrder === ASCEND ? compareResult : -compareResult;
}
}
}
return 0;
})
.map<RecordType>(record => {
const subRecords = (record as any)[childrenColumnName];
if (subRecords) {
return {
...record,
[childrenColumnName]: getSortData(subRecords, sortStates, childrenColumnName),
};
}
return record;
});
}
interface SorterConfig<RecordType> {
prefixCls: Ref<string>;
mergedColumns: Ref<ColumnsType<RecordType>>;
onSorterChange: (
sorterResult: SorterResult<RecordType> | SorterResult<RecordType>[],
sortStates: SortState<RecordType>[],
) => void;
sortDirections: Ref<SortOrder[]>;
tableLocale?: Ref<TableLocale>;
showSorterTooltip?: Ref<boolean | TooltipProps>;
}
export default function useFilterSorter<RecordType>({
prefixCls,
mergedColumns,
onSorterChange,
sortDirections,
tableLocale,
showSorterTooltip,
}: SorterConfig<RecordType>): [
TransformColumns<RecordType>,
Ref<SortState<RecordType>[]>,
Ref<ColumnTitleProps<RecordType>>,
Ref<SorterResult<RecordType> | SorterResult<RecordType>[]>,
] {
const [sortStates, setSortStates] = useState<SortState<RecordType>[]>(
collectSortStates(mergedColumns.value, true),
);
const mergedSorterStates = computed(() => {
let validate = true;
const collectedStates = collectSortStates(mergedColumns.value, false);
// Return if not controlled
if (!collectedStates.length) {
return sortStates.value;
}
const validateStates: SortState<RecordType>[] = [];
function patchStates(state: SortState<RecordType>) {
if (validate) {
validateStates.push(state);
} else {
validateStates.push({
...state,
sortOrder: null,
});
}
}
let multipleMode: boolean | null = null;
collectedStates.forEach(state => {
if (multipleMode === null) {
patchStates(state);
if (state.sortOrder) {
if (state.multiplePriority === false) {
validate = false;
} else {
multipleMode = true;
}
}
} else if (multipleMode && state.multiplePriority !== false) {
patchStates(state);
} else {
validate = false;
patchStates(state);
}
});
return validateStates;
});
// Get render columns title required props
const columnTitleSorterProps = computed<ColumnTitleProps<RecordType>>(() => {
const sortColumns = mergedSorterStates.value.map(({ column, sortOrder }) => ({
column,
order: sortOrder,
}));
return {
sortColumns,
// Legacy
sortColumn: sortColumns[0] && sortColumns[0].column,
sortOrder: (sortColumns[0] && sortColumns[0].order) as SortOrder,
};
});
function triggerSorter(sortState: SortState<RecordType>) {
let newSorterStates;
if (
sortState.multiplePriority === false ||
!mergedSorterStates.value.length ||
mergedSorterStates.value[0].multiplePriority === false
) {
newSorterStates = [sortState];
} else {
newSorterStates = [
...mergedSorterStates.value.filter(({ key }) => key !== sortState.key),
sortState,
];
}
setSortStates(newSorterStates);
onSorterChange(generateSorterInfo(newSorterStates), newSorterStates);
}
const transformColumns = (innerColumns: ColumnsType<RecordType>) =>
injectSorter(
prefixCls.value,
innerColumns,
mergedSorterStates.value,
triggerSorter,
sortDirections.value,
tableLocale.value,
showSorterTooltip.value,
);
const sorters = computed(() => generateSorterInfo(mergedSorterStates.value));
return [transformColumns, mergedSorterStates, columnTitleSorterProps, sorters];
}

View File

@ -0,0 +1,29 @@
import { Ref } from 'vue';
import { TransformColumns, ColumnTitleProps, ColumnsType } from '../interface';
import { renderColumnTitle } from '../util';
function fillTitle<RecordType>(
columns: ColumnsType<RecordType>,
columnTitleProps: ColumnTitleProps<RecordType>,
) {
return columns.map(column => {
const cloneColumn = { ...column };
cloneColumn.title = renderColumnTitle(column.title, columnTitleProps);
if ('children' in cloneColumn) {
cloneColumn.children = fillTitle(cloneColumn.children, columnTitleProps);
}
return cloneColumn;
});
}
export default function useTitleColumns<RecordType>(
columnTitleProps: Ref<ColumnTitleProps<RecordType>>,
): [TransformColumns<RecordType>] {
const filledColumns = (columns: ColumnsType<RecordType>) =>
fillTitle(columns, columnTitleProps.value);
return [filledColumns];
}

View File

@ -1,98 +1,13 @@
import type { App, Plugin } from 'vue';
import { defineComponent } from 'vue';
import T, { defaultTableProps } from './Table';
import type Column from './Column';
import type ColumnGroup from './ColumnGroup';
import {
getOptionProps,
getKey,
getPropsData,
getSlot,
flattenChildren,
} from '../_util/props-util';
import Table from './Table';
import Column from './Column';
import ColumnGroup from './ColumnGroup';
import type { TableProps, TablePaginationConfig } from './Table';
import { App } from 'vue';
export type { ColumnProps } from './Column';
export type { ColumnsType, ColumnType, ColumnGroupType } from './interface';
export type { TableProps, TablePaginationConfig };
const Table = defineComponent({
name: 'ATable',
Column: T.Column,
ColumnGroup: T.ColumnGroup,
inheritAttrs: false,
props: defaultTableProps,
methods: {
normalize(elements = []) {
const flattenElements = flattenChildren(elements);
const columns = [];
flattenElements.forEach(element => {
if (!element) {
return;
}
const key = getKey(element);
const style = element.props?.style || {};
const cls = element.props?.class || '';
const props = getPropsData(element);
const { default: children, ...restSlots } = element.children || {};
const column = { ...restSlots, ...props, style, class: cls };
if (key) {
column.key = key;
}
if (element.type?.__ANT_TABLE_COLUMN_GROUP) {
column.children = this.normalize(typeof children === 'function' ? children() : children);
} else {
const customRender = element.children?.default;
column.customRender = column.customRender || customRender;
}
columns.push(column);
});
return columns;
},
updateColumns(cols = []) {
const columns = [];
const { $slots } = this;
cols.forEach(col => {
const { slots = {}, ...restProps } = col;
const column = {
...restProps,
};
Object.keys(slots).forEach(key => {
const name = slots[key];
if (column[key] === undefined && $slots[name]) {
column[key] = $slots[name];
}
});
// if (slotScopeName && $scopedSlots[slotScopeName]) {
// column.customRender = column.customRender || $scopedSlots[slotScopeName]
// }
if (col.children) {
column.children = this.updateColumns(column.children);
}
columns.push(column);
});
return columns;
},
},
render() {
const { normalize, $slots } = this;
const props: any = { ...getOptionProps(this), ...this.$attrs };
const columns = props.columns ? this.updateColumns(props.columns) : normalize(getSlot(this));
let { title, footer } = props;
const {
title: slotTitle,
footer: slotFooter,
expandedRowRender = props.expandedRowRender,
expandIcon,
} = $slots;
title = title || slotTitle;
footer = footer || slotFooter;
const tProps = {
...props,
columns,
title,
footer,
expandedRowRender,
expandIcon: this.$props.expandIcon || expandIcon,
};
return <T {...tProps} ref="table" />;
},
});
/* istanbul ignore next */
Table.install = function (app: App) {
app.component(Table.name, Table);

111
components/table/index1.tsx Normal file
View File

@ -0,0 +1,111 @@
import type { App, Plugin } from 'vue';
import { defineComponent } from 'vue';
import T, { defaultTableProps } from './Table';
import type Column from './Column';
import type ColumnGroup from './ColumnGroup';
import {
getOptionProps,
getKey,
getPropsData,
getSlot,
flattenChildren,
} from '../_util/props-util';
const Table = defineComponent({
name: 'ATable',
Column: T.Column,
ColumnGroup: T.ColumnGroup,
inheritAttrs: false,
props: defaultTableProps,
methods: {
normalize(elements = []) {
const flattenElements = flattenChildren(elements);
const columns = [];
flattenElements.forEach(element => {
if (!element) {
return;
}
const key = getKey(element);
const style = element.props?.style || {};
const cls = element.props?.class || '';
const props = getPropsData(element);
const { default: children, ...restSlots } = element.children || {};
const column = { ...restSlots, ...props, style, class: cls };
if (key) {
column.key = key;
}
if (element.type?.__ANT_TABLE_COLUMN_GROUP) {
column.children = this.normalize(typeof children === 'function' ? children() : children);
} else {
const customRender = element.children?.default;
column.customRender = column.customRender || customRender;
}
columns.push(column);
});
return columns;
},
updateColumns(cols = []) {
const columns = [];
const { $slots } = this;
cols.forEach(col => {
const { slots = {}, ...restProps } = col;
const column = {
...restProps,
};
Object.keys(slots).forEach(key => {
const name = slots[key];
if (column[key] === undefined && $slots[name]) {
column[key] = $slots[name];
}
});
// if (slotScopeName && $scopedSlots[slotScopeName]) {
// column.customRender = column.customRender || $scopedSlots[slotScopeName]
// }
if (col.children) {
column.children = this.updateColumns(column.children);
}
columns.push(column);
});
return columns;
},
},
render() {
const { normalize, $slots } = this;
const props: any = { ...getOptionProps(this), ...this.$attrs };
const columns = props.columns ? this.updateColumns(props.columns) : normalize(getSlot(this));
let { title, footer } = props;
const {
title: slotTitle,
footer: slotFooter,
expandedRowRender = props.expandedRowRender,
expandIcon,
} = $slots;
title = title || slotTitle;
footer = footer || slotFooter;
const tProps = {
...props,
columns,
title,
footer,
expandedRowRender,
expandIcon: this.$props.expandIcon || expandIcon,
};
return <T {...tProps} ref="table" />;
},
});
/* istanbul ignore next */
Table.install = function (app: App) {
app.component(Table.name, Table);
app.component(Table.Column.name, Table.Column);
app.component(Table.ColumnGroup.name, Table.ColumnGroup);
return app;
};
export const TableColumn = Table.Column;
export const TableColumnGroup = Table.ColumnGroup;
export default Table as typeof Table &
Plugin & {
readonly Column: typeof Column;
readonly ColumnGroup: typeof ColumnGroup;
};

View File

@ -0,0 +1,196 @@
import type {
GetRowKey,
ColumnType as RcColumnType,
RenderedCell as RcRenderedCell,
ExpandableConfig,
DefaultRecordType,
} from '../vc-table/interface';
import type { TooltipProps } from '../tooltip';
import type { CheckboxProps } from '../checkbox';
import type { PaginationProps } from '../pagination';
import { Breakpoint } from '../_util/responsiveObserve';
import { INTERNAL_SELECTION_ITEM } from './hooks/useSelection';
import { tuple, VueNode } from '../_util/type';
// import { TableAction } from './Table';
export type { GetRowKey, ExpandableConfig };
export type Key = string | number;
export type RowSelectionType = 'checkbox' | 'radio';
export type SelectionItemSelectFn = (currentRowKeys: Key[]) => void;
export type ExpandType = null | 'row' | 'nest';
export interface TableLocale {
filterTitle?: string;
filterConfirm?: any;
filterReset?: any;
filterEmptyText?: any;
emptyText?: any | (() => any);
selectAll?: any;
selectNone?: any;
selectInvert?: any;
selectionAll?: any;
sortTitle?: string;
expand?: string;
collapse?: string;
triggerDesc?: string;
triggerAsc?: string;
cancelSort?: string;
}
export type SortOrder = 'descend' | 'ascend' | null;
const TableActions = tuple('paginate', 'sort', 'filter');
export type TableAction = typeof TableActions[number];
export type CompareFn<T> = (a: T, b: T, sortOrder?: SortOrder) => number;
export interface ColumnFilterItem {
text: VueNode;
value: string | number | boolean;
children?: ColumnFilterItem[];
}
export interface ColumnTitleProps<RecordType> {
/** @deprecated Please use `sorterColumns` instead. */
sortOrder?: SortOrder;
/** @deprecated Please use `sorterColumns` instead. */
sortColumn?: ColumnType<RecordType>;
sortColumns?: { column: ColumnType<RecordType>; order: SortOrder }[];
filters?: Record<string, string[]>;
}
export type ColumnTitle<RecordType> = VueNode | ((props: ColumnTitleProps<RecordType>) => VueNode);
export type FilterValue = (Key | boolean)[];
export type FilterKey = Key[] | null;
export interface FilterConfirmProps {
closeDropdown: boolean;
}
export interface FilterDropdownProps {
prefixCls: string;
setSelectedKeys: (selectedKeys: Key[]) => void;
selectedKeys: Key[];
confirm: (param?: FilterConfirmProps) => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
visible: boolean;
}
export interface ColumnType<RecordType = DefaultRecordType> extends RcColumnType<RecordType> {
title?: ColumnTitle<RecordType>;
// Sorter
sorter?:
| boolean
| CompareFn<RecordType>
| {
compare?: CompareFn<RecordType>;
/** Config multiple sorter order priority */
multiple?: number;
};
sortOrder?: SortOrder;
defaultSortOrder?: SortOrder;
sortDirections?: SortOrder[];
showSorterTooltip?: boolean | TooltipProps;
// Filter
filtered?: boolean;
filters?: ColumnFilterItem[];
filterDropdown?: VueNode | ((props: FilterDropdownProps) => VueNode);
filterMultiple?: boolean;
filteredValue?: FilterValue | null;
defaultFilteredValue?: FilterValue | null;
filterIcon?: VueNode | ((filtered: boolean) => VueNode);
onFilter?: (value: string | number | boolean, record: RecordType) => boolean;
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;
// Responsive
responsive?: Breakpoint[];
}
export interface ColumnGroupType<RecordType> extends Omit<ColumnType<RecordType>, 'dataIndex'> {
children: ColumnsType<RecordType>;
}
export type ColumnsType<RecordType = unknown> = (
| ColumnGroupType<RecordType>
| ColumnType<RecordType>
)[];
export interface SelectionItem {
key: string;
text: VueNode;
onSelect?: SelectionItemSelectFn;
}
export type SelectionSelectFn<T> = (
record: T,
selected: boolean,
selectedRows: T[],
nativeEvent: Event,
) => void;
export interface TableRowSelection<T> {
/** Keep the selection keys in list even the key not exist in `dataSource` anymore */
preserveSelectedRowKeys?: boolean;
type?: RowSelectionType;
selectedRowKeys?: Key[];
defaultSelectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: T[]) => void;
getCheckboxProps?: (record: T) => Partial<Omit<CheckboxProps, 'checked' | 'defaultChecked'>>;
onSelect?: SelectionSelectFn<T>;
onSelectMultiple?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectInvert?: (selectedRowKeys: Key[]) => void;
onSelectNone?: () => void;
selections?: INTERNAL_SELECTION_ITEM[] | boolean;
hideSelectAll?: boolean;
fixed?: boolean;
columnWidth?: string | number;
columnTitle?: string | VueNode;
checkStrictly?: boolean;
renderCell?: (
value: boolean,
record: T,
index: number,
originNode: VueNode,
) => VueNode | RcRenderedCell<T>;
}
export type TransformColumns<RecordType> = (
columns: ColumnsType<RecordType>,
) => ColumnsType<RecordType>;
export interface TableCurrentDataSource<RecordType> {
currentDataSource: RecordType[];
action: TableAction;
}
export interface SorterResult<RecordType = DefaultRecordType> {
column?: ColumnType<RecordType>;
order?: SortOrder;
field?: Key | readonly Key[];
columnKey?: Key;
}
export type GetPopupContainer = (triggerNode: HTMLElement) => HTMLElement;
type TablePaginationPosition =
| 'topLeft'
| 'topCenter'
| 'topRight'
| 'bottomLeft'
| 'bottomCenter'
| 'bottomRight';
export interface TablePaginationConfig extends PaginationProps {
position?: TablePaginationPosition[];
}

View File

@ -14,7 +14,7 @@ export const ColumnFilterItem = PropTypes.shape({
}).loose;
export const columnProps = {
title: PropTypes.VNodeChild,
title: PropTypes.any,
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
dataIndex: PropTypes.string,
customRender: PropTypes.func,
@ -49,7 +49,6 @@ export const columnProps = {
),
sortDirections: PropTypes.array,
// children?: ColumnProps<T>[];
// onCellClick?: (record: T, event: any) => void;
// onCell?: (record: T) => any;
// onHeaderCell?: (props: ColumnProps<T>) => any;
};

View File

@ -0,0 +1,129 @@
@import './index';
@import './size';
@table-border: @border-width-base @border-style-base @table-border-color;
.@{table-prefix-cls}.@{table-prefix-cls}-bordered {
// ============================ Title =============================
> .@{table-prefix-cls}-title {
border: @table-border;
border-bottom: 0;
}
> .@{table-prefix-cls}-container {
// ============================ Content ============================
border: @table-border;
border-right: 0;
border-bottom: 0;
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-header,
> .@{table-prefix-cls}-body,
> .@{table-prefix-cls}-summary {
> table {
// ============================= Cell =============================
> thead > tr > th,
> tbody > tr > td,
> tfoot > tr > th,
> tfoot > tr > td {
border-right: @table-border;
}
// ============================ Header ============================
> thead {
> tr:not(:last-child) > th {
border-bottom: @border-width-base @border-style-base @table-border-color;
}
> tr > th {
&::before {
background-color: transparent !important;
}
}
}
// Fixed right should provides additional border
> thead > tr,
> tbody > tr,
> tfoot > tr {
> .@{table-prefix-cls}-cell-fix-right-first::after {
border-right: @table-border;
}
}
}
// ========================== Expandable ==========================
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical (-@table-padding-horizontal - @border-width-base);
&::after {
position: absolute;
top: 0;
right: @border-width-base;
bottom: 0;
border-right: @table-border;
content: '';
}
}
}
}
}
&.@{table-prefix-cls}-scroll-horizontal {
> .@{table-prefix-cls}-container > .@{table-prefix-cls}-body {
> table > tbody {
> tr.@{table-prefix-cls}-expanded-row,
> tr.@{table-prefix-cls}-placeholder {
> td {
border-right: 0;
}
}
}
}
}
// Size related
&.@{table-prefix-cls}-middle {
> .@{table-prefix-cls}-container {
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-body {
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-md (-@table-padding-horizontal-md - @border-width-base);
}
}
}
}
}
&.@{table-prefix-cls}-small {
> .@{table-prefix-cls}-container {
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-body {
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-sm (-@table-padding-horizontal-sm - @border-width-base);
}
}
}
}
}
// ============================ Footer ============================
> .@{table-prefix-cls}-footer {
border: @table-border;
border-top: 0;
}
}
.@{table-prefix-cls}-cell {
// ============================ Nested ============================
.@{table-prefix-cls}-container:first-child {
// :first-child to avoid the case when bordered and title is set
border-top: 0;
}
&-scrollbar {
box-shadow: 0 @border-width-base 0 @border-width-base @table-header-bg;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,12 @@ import './index.less';
// style dependencies
// deps-lint-skip: menu
// deps-lint-skip: grid
import '../../button/style';
import '../../empty/style';
import '../../radio/style';
import '../../checkbox/style';
import '../../dropdown/style';
import '../../spin/style';
import '../../pagination/style';
import '../../tooltip/style';

View File

@ -0,0 +1,45 @@
// ================================================================
// = Border Radio =
// ================================================================
.@{table-prefix-cls} {
/* title + table */
&-title {
border-radius: @table-border-radius-base @table-border-radius-base 0 0;
}
&-title + &-container {
border-top-left-radius: 0;
border-top-right-radius: 0;
table > thead > tr:first-child {
th:first-child {
border-radius: 0;
}
th:last-child {
border-radius: 0;
}
}
}
/* table */
&-container {
border-top-left-radius: @table-border-radius-base;
border-top-right-radius: @table-border-radius-base;
table > thead > tr:first-child {
th:first-child {
border-top-left-radius: @table-border-radius-base;
}
th:last-child {
border-top-right-radius: @table-border-radius-base;
}
}
}
/* table + footer */
&-footer {
border-radius: 0 0 @table-border-radius-base @table-border-radius-base;
}
}

View File

@ -0,0 +1,162 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@table-prefix-cls: ~'@{ant-prefix}-table';
@table-wrapepr-cls: ~'@{table-prefix-cls}-wrapper';
@table-wrapepr-rtl-cls: ~'@{table-prefix-cls}-wrapper-rtl';
.@{table-prefix-cls}-wrapper {
&-rtl {
direction: rtl;
}
}
.@{table-prefix-cls} {
&-rtl {
direction: rtl;
}
table {
.@{table-wrapepr-rtl-cls} & {
text-align: right;
}
}
// ============================ Header ============================
&-thead {
> tr {
> th {
&[colspan]:not([colspan='1']) {
.@{table-wrapepr-rtl-cls} & {
text-align: center;
}
}
.@{table-wrapepr-rtl-cls} & {
text-align: right;
}
}
}
}
// ============================= Body =============================
&-tbody {
> tr {
// ========================= Nest Table ===========================
.@{table-prefix-cls}-wrapper:only-child {
.@{table-prefix-cls}.@{table-prefix-cls}-rtl {
margin: -@table-padding-vertical (@table-padding-horizontal + ceil(@font-size-sm * 1.4)) -@table-padding-vertical -@table-padding-horizontal;
}
}
}
}
// ========================== Pagination ==========================
&-pagination {
&-left {
.@{table-wrapepr-cls}.@{table-wrapepr-rtl-cls} & {
justify-content: flex-end;
}
}
&-right {
.@{table-wrapepr-cls}.@{table-wrapepr-rtl-cls} & {
justify-content: flex-start;
}
}
}
// ================================================================
// = Function =
// ================================================================
// ============================ Sorter ============================
&-column-sorter {
.@{table-wrapepr-rtl-cls} & {
margin-right: @padding-xs;
margin-left: 0;
}
}
// ============================ Filter ============================
&-filter-column-title {
.@{table-wrapepr-rtl-cls} & {
padding: @table-padding-vertical @table-padding-horizontal @table-padding-vertical 2.3em;
}
}
&-thead tr th.@{table-prefix-cls}-column-has-sorters {
.@{table-prefix-cls}-filter-column-title {
.@{table-prefix-cls}-rtl & {
padding: 0 0 0 2.3em;
}
}
}
&-filter-trigger-container {
.@{table-wrapepr-rtl-cls} & {
right: auto;
left: 0;
}
}
// Dropdown
&-filter-dropdown {
// Checkbox
&,
&-submenu {
.@{ant-prefix}-checkbox-wrapper + span {
.@{ant-prefix}-dropdown-rtl &,
.@{ant-prefix}-dropdown-menu-submenu-rtl& {
padding-right: 8px;
padding-left: 0;
}
}
}
}
// ========================== Selections ==========================
&-selection {
.@{table-wrapepr-rtl-cls} & {
text-align: center;
}
}
// ========================== Expandable ==========================
&-row-indent {
.@{table-wrapepr-rtl-cls} & {
float: right;
}
}
&-row-expand-icon {
.@{table-wrapepr-rtl-cls} & {
float: right;
}
.@{table-prefix-cls}-row-indent + & {
.@{table-wrapepr-rtl-cls} & {
margin-right: 0;
margin-left: @padding-xs;
}
}
&::after {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(-90deg);
}
}
&-collapsed::before {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(180deg);
}
}
&-collapsed::after {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(0deg);
}
}
}
}

View File

@ -1,38 +1,34 @@
@table-padding-vertical-md: (@table-padding-vertical * 3 / 4);
@table-padding-horizontal-md: (@table-padding-horizontal / 2);
@table-padding-vertical-sm: (@table-padding-vertical / 2);
@table-padding-horizontal-sm: (@table-padding-horizontal / 2);
@import './index';
.table-size(@size, @padding-vertical, @padding-horizontal) {
.table-size(@size, @padding-vertical, @padding-horizontal, @font-size) {
.@{table-prefix-cls}.@{table-prefix-cls}-@{size} {
> .@{table-prefix-cls}-title,
> .@{table-prefix-cls}-content > .@{table-prefix-cls}-footer {
font-size: @font-size;
.@{table-prefix-cls}-title,
.@{table-prefix-cls}-footer,
.@{table-prefix-cls}-thead > tr > th,
.@{table-prefix-cls}-tbody > tr > td,
tfoot > tr > th,
tfoot > tr > td {
padding: @padding-vertical @padding-horizontal;
}
> .@{table-prefix-cls}-content {
> .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-left
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table,
> .@{table-prefix-cls}-fixed-right
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table {
> .@{table-prefix-cls}-thead > tr > th,
> .@{table-prefix-cls}-tbody > tr > td {
padding: @padding-vertical @padding-horizontal;
}
}
.@{table-prefix-cls}-filter-trigger {
margin-right: -(@padding-horizontal / 2);
}
tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper {
margin: -@padding-vertical (-@table-padding-horizontal / 2) -@padding-vertical - 1px;
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@padding-vertical -@padding-horizontal;
}
.@{table-prefix-cls}-tbody {
// ========================= Nest Table ===========================
.@{table-prefix-cls}-wrapper:only-child {
.@{table-prefix-cls} {
margin: -@padding-vertical -@padding-horizontal -@padding-vertical (@padding-horizontal +
ceil((@font-size-sm * 1.4)));
}
}
}
}
}
@ -40,14 +36,17 @@
// ================================================================
// = Middle =
// ================================================================
.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md);
.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md, @table-font-size-md);
// ================================================================
// = Small =
// ================================================================
.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm);
.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm, @table-font-size-sm);
.@{table-prefix-cls}-small {
.@{table-prefix-cls}-thead > tr > th {
background-color: @table-header-bg-sm;
}
.@{table-prefix-cls}-selection-column {
width: 46px;
min-width: 46px;

View File

@ -1,73 +1,27 @@
export function flatArray(data = [], childrenName = 'children') {
const result = [];
const loop = array => {
array.forEach(item => {
if (item[childrenName]) {
const newItem = { ...item };
delete newItem[childrenName];
result.push(newItem);
if (item[childrenName].length > 0) {
loop(item[childrenName]);
}
} else {
result.push(item);
}
});
};
loop(data);
return result;
import type { ColumnType, ColumnTitle, ColumnTitleProps, Key } from './interface';
export function getColumnKey<RecordType>(column: ColumnType<RecordType>, defaultKey: string): Key {
if ('key' in column && column.key !== undefined && column.key !== null) {
return column.key;
}
if (column.dataIndex) {
return (Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex) as Key;
}
return defaultKey;
}
export function treeMap(tree, mapper, childrenName = 'children') {
return tree.map((node, index) => {
const extra = {};
if (node[childrenName]) {
extra[childrenName] = treeMap(node[childrenName], mapper, childrenName);
}
return {
...mapper(node, index),
...extra,
};
});
export function getColumnPos(index: number, pos?: string) {
return pos ? `${pos}-${index}` : `${index}`;
}
export function flatFilter(tree, callback) {
return tree.reduce((acc, node) => {
if (callback(node)) {
acc.push(node);
}
if (node.children) {
const children = flatFilter(node.children, callback);
acc.push(...children);
}
return acc;
}, []);
}
export function renderColumnTitle<RecordType>(
title: ColumnTitle<RecordType>,
props: ColumnTitleProps<RecordType>,
) {
if (typeof title === 'function') {
return title(props);
}
// export function normalizeColumns (elements) {
// const columns = []
// React.Children.forEach(elements, (element) => {
// if (!React.isValidElement(element)) {
// return
// }
// const column = {
// ...element.props,
// }
// if (element.key) {
// column.key = element.key
// }
// if (element.type && element.type.__ANT_TABLE_COLUMN_GROUP) {
// column.children = normalizeColumns(column.children)
// }
// columns.push(column)
// })
// return columns
// }
export function generateValueMaps(items, maps = {}) {
(items || []).forEach(({ value, children }) => {
maps[value.toString()] = value;
generateValueMaps(children, maps);
});
return maps;
return title;
}

View File

@ -6,8 +6,6 @@ import type {
Key,
TriggerEventHandler,
GetComponentProps,
ExpandableConfig,
LegacyExpandableProps,
PanelRender,
TableLayout,
RowClassName,
@ -15,6 +13,8 @@ import type {
ColumnType,
CustomizeScrollBody,
TableSticky,
ExpandedRowRender,
RenderExpandIcon,
} from './interface';
import Body from './Body';
import useColumns from './hooks/useColumns';
@ -29,7 +29,7 @@ import { getCellFixedInfo } from './utils/fixUtil';
import StickyScrollBar from './stickyScrollBar';
import useSticky from './hooks/useSticky';
import FixedHolder from './FixedHolder';
import type { CSSProperties } from 'vue';
import type { CSSProperties, Ref } from 'vue';
import {
computed,
defineComponent,
@ -63,7 +63,7 @@ const EMPTY_SCROLL_TARGET = {};
export const INTERNAL_HOOKS = 'rc-table-internal-hook';
export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<RecordType> {
export interface TableProps<RecordType = unknown> {
prefixCls?: string;
data?: RecordType[];
columns?: ColumnsType<RecordType>;
@ -73,10 +73,6 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
// Fixed Columns
scroll?: { x?: number | true | string; y?: number | string };
// Expandable
/** Config expand rows */
expandable?: ExpandableConfig<RecordType>;
indentSize?: number;
rowClassName?: string | RowClassName<RecordType>;
// Additional Part
@ -94,17 +90,94 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
direction?: 'ltr' | 'rtl';
// Expandable
expandFixed?: boolean;
expandColumnWidth?: number;
expandedRowKeys?: Key[];
defaultExpandedRowKeys?: Key[];
expandedRowRender?: ExpandedRowRender<RecordType>;
expandRowByClick?: boolean;
expandIcon?: RenderExpandIcon<RecordType>;
onExpand?: (expanded: boolean, record: RecordType) => void;
onExpandedRowsChange?: (expandedKeys: Key[]) => void;
defaultExpandAllRows?: boolean;
indentSize?: number;
expandIconColumnIndex?: number;
expandedRowClassName?: RowClassName<RecordType>;
childrenColumnName?: string;
rowExpandable?: (record: RecordType) => boolean;
// =================================== Internal ===================================
/**
* @private Internal usage, may remove by refactor. Should always use `columns` instead.
*
* !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
*/
internalHooks?: string;
/**
* @private Internal usage, may remove by refactor. Should always use `columns` instead.
*
* !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
*/
// Used for antd table transform column with additional column
transformColumns?: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>;
/**
* @private Internal usage, may remove by refactor.
*
* !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
*/
internalRefs?: {
body: Ref<HTMLDivElement>;
};
sticky?: boolean | TableSticky;
canExpandable?: boolean;
}
export default defineComponent<TableProps>({
name: 'Table',
slots: ['title', 'footer', 'summary', 'emptyText'],
emits: ['expand', 'expandedRowsChange'],
props: [
'prefixCls',
'data',
'columns',
'rowKey',
'tableLayout',
'scroll',
'rowClassName',
'title',
'footer',
'id',
'showHeader',
'components',
'customRow',
'customHeaderRow',
'direction',
'expandFixed',
'expandColumnWidth',
'expandedRowKeys',
'defaultExpandedRowKeys',
'expandedRowRender',
'expandRowByClick',
'expandIcon',
'onExpand',
'onExpandedRowsChange',
'defaultExpandAllRows',
'indentSize',
'expandIconColumnIndex',
'expandedRowClassName',
'childrenColumnName',
'rowExpandable',
'sticky',
'transformColumns',
'internalHooks',
'internalRefs',
'canExpandable',
] as any,
setup(props, { slots, emit }) {
const mergedData = computed(() => props.data || EMPTY_DATA);
const hasData = computed(() => !!mergedData.value.length);
@ -157,6 +230,7 @@ export default defineComponent<TableProps>({
* Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor
*/
if (
props.canExpandable ||
mergedData.value.some(
record => record && typeof record === 'object' && record[mergedChildrenColumnName.value],
)
@ -203,16 +277,19 @@ export default defineComponent<TableProps>({
const componentWidth = ref(0);
const [columns, flattenColumns] = useColumns({
...toRefs(props),
const [columns, flattenColumns] = useColumns(
{
...toRefs(props),
// children,
expandable: computed(() => !!props.expandedRowRender),
expandedKeys: mergedExpandedKeys,
getRowKey,
onTriggerExpand,
expandIcon: mergedExpandIcon,
});
// children,
expandable: computed(() => !!props.expandedRowRender),
expandedKeys: mergedExpandedKeys,
getRowKey,
onTriggerExpand,
expandIcon: mergedExpandIcon,
},
computed(() => (props.internalHooks === INTERNAL_HOOKS ? props.transformColumns : null)),
);
const columnContext = computed(() => ({
columns: columns.value,
@ -377,6 +454,15 @@ export default defineComponent<TableProps>({
});
});
watchEffect(
() => {
if (props.internalHooks === INTERNAL_HOOKS && props.internalRefs) {
props.internalRefs.body.value = scrollBodyRef.value;
}
},
{ flush: 'post' },
);
// Table layout
const mergedTableLayout = computed(() => {
if (props.tableLayout) {

View File

@ -103,37 +103,39 @@ function revertForRtl<RecordType>(columns: ColumnsType<RecordType>): ColumnsType
/**
* Parse `columns` & `children` into `columns`.
*/
function useColumns<RecordType>({
prefixCls,
columns: baseColumns,
// children,
expandable,
expandedKeys,
getRowKey,
onTriggerExpand,
expandIcon,
rowExpandable,
expandIconColumnIndex,
direction,
expandRowByClick,
expandColumnWidth,
expandFixed,
}: {
prefixCls?: Ref<string>;
columns?: Ref<ColumnsType<RecordType>>;
expandable: Ref<boolean>;
expandedKeys: Ref<Set<Key>>;
getRowKey: Ref<GetRowKey<RecordType>>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
rowExpandable?: Ref<(record: RecordType) => boolean>;
expandIconColumnIndex?: Ref<number>;
direction?: Ref<'ltr' | 'rtl'>;
expandRowByClick?: Ref<boolean>;
expandColumnWidth?: Ref<number | string>;
expandFixed?: Ref<FixedType>;
}): // transformColumns: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>,
[ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
function useColumns<RecordType>(
{
prefixCls,
columns: baseColumns,
// children,
expandable,
expandedKeys,
getRowKey,
onTriggerExpand,
expandIcon,
rowExpandable,
expandIconColumnIndex,
direction,
expandRowByClick,
expandColumnWidth,
expandFixed,
}: {
prefixCls?: Ref<string>;
columns?: Ref<ColumnsType<RecordType>>;
expandable: Ref<boolean>;
expandedKeys: Ref<Set<Key>>;
getRowKey: Ref<GetRowKey<RecordType>>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
rowExpandable?: Ref<(record: RecordType) => boolean>;
expandIconColumnIndex?: Ref<number>;
direction?: Ref<'ltr' | 'rtl'>;
expandRowByClick?: Ref<boolean>;
expandColumnWidth?: Ref<number | string>;
expandFixed?: Ref<FixedType>;
},
transformColumns: Ref<(columns: ColumnsType<RecordType>) => ColumnsType<RecordType>>,
): [ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
// Add expand column
const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
if (expandable.value) {
@ -196,9 +198,9 @@ function useColumns<RecordType>({
const mergedColumns = computed(() => {
let finalColumns = withExpandColumns.value;
// if (transformColumns) {
// finalColumns = transformColumns(finalColumns);
// }
if (transformColumns.value) {
finalColumns = transformColumns.value(finalColumns);
}
// Always provides at least one column for table display
if (!finalColumns.length) {