refactor: table
parent
de233c9c97
commit
456e3ae404
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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
|
||||
// --
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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} />;
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
const FilterDropdownMenuWrapper = (_props, { slots }) => (
|
||||
<div onClick={e => e.stopPropagation()}>{slots.default?.()}</div>
|
||||
);
|
||||
|
||||
export default FilterDropdownMenuWrapper;
|
|
@ -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;
|
|
@ -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];
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue