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 PropTypes from '../_util/vue-types';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import VcCheckbox from '../vc-checkbox';
|
import VcCheckbox from '../vc-checkbox';
|
||||||
|
@ -9,11 +9,8 @@ import type { RadioChangeEvent } from '../radio/interface';
|
||||||
import type { EventHandler } from '../_util/EventInterface';
|
import type { EventHandler } from '../_util/EventInterface';
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|
||||||
export default defineComponent({
|
export const checkboxProps = () => {
|
||||||
name: 'ACheckbox',
|
return {
|
||||||
inheritAttrs: false,
|
|
||||||
__ANT_CHECKBOX: true,
|
|
||||||
props: {
|
|
||||||
prefixCls: PropTypes.string,
|
prefixCls: PropTypes.string,
|
||||||
defaultChecked: PropTypes.looseBool,
|
defaultChecked: PropTypes.looseBool,
|
||||||
checked: PropTypes.looseBool,
|
checked: PropTypes.looseBool,
|
||||||
|
@ -27,7 +24,17 @@ export default defineComponent({
|
||||||
autofocus: PropTypes.looseBool,
|
autofocus: PropTypes.looseBool,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
'onUpdate:checked': 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'],
|
emits: ['change', 'update:checked'],
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
|
@ -38,6 +45,9 @@ export default defineComponent({
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value(value, prevValue) {
|
value(value, prevValue) {
|
||||||
|
if (this.skipGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const { checkboxGroupContext: checkboxGroup = {} } = this;
|
const { checkboxGroupContext: checkboxGroup = {} } = this;
|
||||||
if (checkboxGroup.registerValue && checkboxGroup.cancelValue) {
|
if (checkboxGroup.registerValue && checkboxGroup.cancelValue) {
|
||||||
|
@ -85,7 +95,7 @@ export default defineComponent({
|
||||||
const props = getOptionProps(this);
|
const props = getOptionProps(this);
|
||||||
const { checkboxGroupContext: checkboxGroup, $attrs } = this;
|
const { checkboxGroupContext: checkboxGroup, $attrs } = this;
|
||||||
const children = getSlot(this);
|
const children = getSlot(this);
|
||||||
const { indeterminate, prefixCls: customizePrefixCls, ...restProps } = props;
|
const { indeterminate, prefixCls: customizePrefixCls, skipGroup, ...restProps } = props;
|
||||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
const getPrefixCls = this.configProvider.getPrefixCls;
|
||||||
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
|
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
|
||||||
const {
|
const {
|
||||||
|
@ -101,7 +111,7 @@ export default defineComponent({
|
||||||
prefixCls,
|
prefixCls,
|
||||||
...restAttrs,
|
...restAttrs,
|
||||||
};
|
};
|
||||||
if (checkboxGroup) {
|
if (checkboxGroup && !skipGroup) {
|
||||||
checkboxProps.onChange = (...args) => {
|
checkboxProps.onChange = (...args) => {
|
||||||
this.$emit('change', ...args);
|
this.$emit('change', ...args);
|
||||||
checkboxGroup.toggleOption({ label: children, value: props.value });
|
checkboxGroup.toggleOption({ label: children, value: props.value });
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { App, Plugin } from 'vue';
|
import type { App, Plugin } from 'vue';
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox, { checkboxProps } from './Checkbox';
|
||||||
import CheckboxGroup from './Group';
|
import CheckboxGroup from './Group';
|
||||||
|
export type { CheckboxProps } from './Checkbox';
|
||||||
|
|
||||||
Checkbox.Group = CheckboxGroup;
|
Checkbox.Group = CheckboxGroup;
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ Checkbox.install = function (app: App) {
|
||||||
app.component(CheckboxGroup.name, CheckboxGroup);
|
app.component(CheckboxGroup.name, CheckboxGroup);
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
export { CheckboxGroup };
|
export { CheckboxGroup, checkboxProps };
|
||||||
export default Checkbox as typeof Checkbox &
|
export default Checkbox as typeof Checkbox &
|
||||||
Plugin & {
|
Plugin & {
|
||||||
readonly Group: typeof CheckboxGroup;
|
readonly Group: typeof CheckboxGroup;
|
||||||
|
|
|
@ -238,11 +238,6 @@ const TableRow = {
|
||||||
for (let i = 0; i < columns.length; i += 1) {
|
for (let i = 0; i < columns.length; i += 1) {
|
||||||
const column = columns[i];
|
const column = columns[i];
|
||||||
|
|
||||||
warning(
|
|
||||||
column.onCellClick === undefined,
|
|
||||||
'column[onCellClick] is deprecated, please use column[customCell] instead.',
|
|
||||||
);
|
|
||||||
|
|
||||||
cells.push(
|
cells.push(
|
||||||
<TableCell
|
<TableCell
|
||||||
prefixCls={prefixCls}
|
prefixCls={prefixCls}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { ValidateMessages } from '../form/interface';
|
||||||
import type { TransferLocale } from '../transfer';
|
import type { TransferLocale } from '../transfer';
|
||||||
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
|
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
|
||||||
import type { PaginationLocale } from '../pagination/Pagination';
|
import type { PaginationLocale } from '../pagination/Pagination';
|
||||||
|
import { TableLocale } from '../table/interface';
|
||||||
|
|
||||||
interface TransferLocaleForEmpty {
|
interface TransferLocaleForEmpty {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -16,7 +17,7 @@ interface TransferLocaleForEmpty {
|
||||||
export interface Locale {
|
export interface Locale {
|
||||||
locale: string;
|
locale: string;
|
||||||
Pagination?: PaginationLocale;
|
Pagination?: PaginationLocale;
|
||||||
Table?: Record<string, any>;
|
Table?: TableLocale;
|
||||||
Popconfirm?: Record<string, any>;
|
Popconfirm?: Record<string, any>;
|
||||||
Upload?: Record<string, any>;
|
Upload?: Record<string, any>;
|
||||||
Form?: {
|
Form?: {
|
||||||
|
|
|
@ -556,8 +556,8 @@
|
||||||
@table-header-bg: @background-color-light;
|
@table-header-bg: @background-color-light;
|
||||||
@table-header-color: @heading-color;
|
@table-header-color: @heading-color;
|
||||||
@table-header-sort-bg: @background-color-base;
|
@table-header-sort-bg: @background-color-base;
|
||||||
@table-body-sort-bg: rgba(0, 0, 0, 0.01);
|
@table-body-sort-bg: #fafafa;
|
||||||
@table-row-hover-bg: @primary-1;
|
@table-row-hover-bg: @background-color-light;
|
||||||
@table-selected-row-color: inherit;
|
@table-selected-row-color: inherit;
|
||||||
@table-selected-row-bg: @primary-1;
|
@table-selected-row-bg: @primary-1;
|
||||||
@table-body-selected-sort-bg: @table-selected-row-bg;
|
@table-body-selected-sort-bg: @table-selected-row-bg;
|
||||||
|
@ -565,15 +565,31 @@
|
||||||
@table-expanded-row-bg: #fbfbfb;
|
@table-expanded-row-bg: #fbfbfb;
|
||||||
@table-padding-vertical: 16px;
|
@table-padding-vertical: 16px;
|
||||||
@table-padding-horizontal: 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-border-radius-base: @border-radius-base;
|
||||||
@table-footer-bg: @background-color-light;
|
@table-footer-bg: @background-color-light;
|
||||||
@table-footer-color: @heading-color;
|
@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
|
// Sorter
|
||||||
// Legacy: `table-header-sort-active-bg` is used for hover not real active
|
// Legacy: `table-header-sort-active-bg` is used for hover not real active
|
||||||
@table-header-sort-active-bg: darken(@table-header-bg, 3%);
|
@table-header-sort-active-bg: rgba(0, 0, 0, 0.04);
|
||||||
// Filter
|
// 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
|
// Tag
|
||||||
// --
|
// --
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { defineComponent } from 'vue';
|
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',
|
name: 'ATableColumn',
|
||||||
props: columnProps,
|
slots: ['title', 'filterIcon'],
|
||||||
render() {
|
render() {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import PropTypes, { withUndefined } from '../_util/vue-types';
|
import { ColumnGroupProps } from '../vc-table/sugar/ColumnGroup';
|
||||||
import { tuple } from '../_util/type';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent<ColumnGroupProps<any>>({
|
||||||
name: 'ATableColumnGroup',
|
name: 'ATableColumnGroup',
|
||||||
props: {
|
slots: ['title'],
|
||||||
fixed: withUndefined(
|
|
||||||
PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('left', 'right'))]),
|
|
||||||
),
|
|
||||||
title: PropTypes.any,
|
|
||||||
},
|
|
||||||
__ANT_TABLE_COLUMN_GROUP: true,
|
__ANT_TABLE_COLUMN_GROUP: true,
|
||||||
render() {
|
render() {
|
||||||
return null;
|
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 Table from './Table';
|
||||||
import { defineComponent } from 'vue';
|
import Column from './Column';
|
||||||
import T, { defaultTableProps } from './Table';
|
import ColumnGroup from './ColumnGroup';
|
||||||
import type Column from './Column';
|
import type { TableProps, TablePaginationConfig } from './Table';
|
||||||
import type ColumnGroup from './ColumnGroup';
|
import { App } from 'vue';
|
||||||
import {
|
|
||||||
getOptionProps,
|
export type { ColumnProps } from './Column';
|
||||||
getKey,
|
export type { ColumnsType, ColumnType, ColumnGroupType } from './interface';
|
||||||
getPropsData,
|
export type { TableProps, TablePaginationConfig };
|
||||||
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 */
|
/* istanbul ignore next */
|
||||||
Table.install = function (app: App) {
|
Table.install = function (app: App) {
|
||||||
app.component(Table.name, Table);
|
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;
|
}).loose;
|
||||||
|
|
||||||
export const columnProps = {
|
export const columnProps = {
|
||||||
title: PropTypes.VNodeChild,
|
title: PropTypes.any,
|
||||||
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
dataIndex: PropTypes.string,
|
dataIndex: PropTypes.string,
|
||||||
customRender: PropTypes.func,
|
customRender: PropTypes.func,
|
||||||
|
@ -49,7 +49,6 @@ export const columnProps = {
|
||||||
),
|
),
|
||||||
sortDirections: PropTypes.array,
|
sortDirections: PropTypes.array,
|
||||||
// children?: ColumnProps<T>[];
|
// children?: ColumnProps<T>[];
|
||||||
// onCellClick?: (record: T, event: any) => void;
|
|
||||||
// onCell?: (record: T) => any;
|
// onCell?: (record: T) => any;
|
||||||
// onHeaderCell?: (props: ColumnProps<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
|
// style dependencies
|
||||||
// deps-lint-skip: menu
|
// deps-lint-skip: menu
|
||||||
|
// deps-lint-skip: grid
|
||||||
|
import '../../button/style';
|
||||||
import '../../empty/style';
|
import '../../empty/style';
|
||||||
import '../../radio/style';
|
import '../../radio/style';
|
||||||
import '../../checkbox/style';
|
import '../../checkbox/style';
|
||||||
import '../../dropdown/style';
|
import '../../dropdown/style';
|
||||||
import '../../spin/style';
|
import '../../spin/style';
|
||||||
import '../../pagination/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);
|
@import './index';
|
||||||
@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-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}.@{table-prefix-cls}-@{size} {
|
||||||
> .@{table-prefix-cls}-title,
|
font-size: @font-size;
|
||||||
> .@{table-prefix-cls}-content > .@{table-prefix-cls}-footer {
|
|
||||||
|
.@{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;
|
padding: @padding-vertical @padding-horizontal;
|
||||||
}
|
}
|
||||||
> .@{table-prefix-cls}-content {
|
|
||||||
> .@{table-prefix-cls}-header > table,
|
.@{table-prefix-cls}-filter-trigger {
|
||||||
> .@{table-prefix-cls}-body > table,
|
margin-right: -(@padding-horizontal / 2);
|
||||||
> .@{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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper {
|
.@{table-prefix-cls}-expanded-row-fixed {
|
||||||
margin: -@padding-vertical (-@table-padding-horizontal / 2) -@padding-vertical - 1px;
|
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 =
|
// = 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 =
|
// = 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}-small {
|
||||||
|
.@{table-prefix-cls}-thead > tr > th {
|
||||||
|
background-color: @table-header-bg-sm;
|
||||||
|
}
|
||||||
.@{table-prefix-cls}-selection-column {
|
.@{table-prefix-cls}-selection-column {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
min-width: 46px;
|
min-width: 46px;
|
||||||
|
|
|
@ -1,73 +1,27 @@
|
||||||
export function flatArray(data = [], childrenName = 'children') {
|
import type { ColumnType, ColumnTitle, ColumnTitleProps, Key } from './interface';
|
||||||
const result = [];
|
|
||||||
const loop = array => {
|
export function getColumnKey<RecordType>(column: ColumnType<RecordType>, defaultKey: string): Key {
|
||||||
array.forEach(item => {
|
if ('key' in column && column.key !== undefined && column.key !== null) {
|
||||||
if (item[childrenName]) {
|
return column.key;
|
||||||
const newItem = { ...item };
|
}
|
||||||
delete newItem[childrenName];
|
if (column.dataIndex) {
|
||||||
result.push(newItem);
|
return (Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex) as Key;
|
||||||
if (item[childrenName].length > 0) {
|
}
|
||||||
loop(item[childrenName]);
|
|
||||||
}
|
return defaultKey;
|
||||||
} else {
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
loop(data);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function treeMap(tree, mapper, childrenName = 'children') {
|
export function getColumnPos(index: number, pos?: string) {
|
||||||
return tree.map((node, index) => {
|
return pos ? `${pos}-${index}` : `${index}`;
|
||||||
const extra = {};
|
|
||||||
if (node[childrenName]) {
|
|
||||||
extra[childrenName] = treeMap(node[childrenName], mapper, childrenName);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...mapper(node, index),
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flatFilter(tree, callback) {
|
export function renderColumnTitle<RecordType>(
|
||||||
return tree.reduce((acc, node) => {
|
title: ColumnTitle<RecordType>,
|
||||||
if (callback(node)) {
|
props: ColumnTitleProps<RecordType>,
|
||||||
acc.push(node);
|
) {
|
||||||
}
|
if (typeof title === 'function') {
|
||||||
if (node.children) {
|
return title(props);
|
||||||
const children = flatFilter(node.children, callback);
|
}
|
||||||
acc.push(...children);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// export function normalizeColumns (elements) {
|
return title;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ import type {
|
||||||
Key,
|
Key,
|
||||||
TriggerEventHandler,
|
TriggerEventHandler,
|
||||||
GetComponentProps,
|
GetComponentProps,
|
||||||
ExpandableConfig,
|
|
||||||
LegacyExpandableProps,
|
|
||||||
PanelRender,
|
PanelRender,
|
||||||
TableLayout,
|
TableLayout,
|
||||||
RowClassName,
|
RowClassName,
|
||||||
|
@ -15,6 +13,8 @@ import type {
|
||||||
ColumnType,
|
ColumnType,
|
||||||
CustomizeScrollBody,
|
CustomizeScrollBody,
|
||||||
TableSticky,
|
TableSticky,
|
||||||
|
ExpandedRowRender,
|
||||||
|
RenderExpandIcon,
|
||||||
} from './interface';
|
} from './interface';
|
||||||
import Body from './Body';
|
import Body from './Body';
|
||||||
import useColumns from './hooks/useColumns';
|
import useColumns from './hooks/useColumns';
|
||||||
|
@ -29,7 +29,7 @@ import { getCellFixedInfo } from './utils/fixUtil';
|
||||||
import StickyScrollBar from './stickyScrollBar';
|
import StickyScrollBar from './stickyScrollBar';
|
||||||
import useSticky from './hooks/useSticky';
|
import useSticky from './hooks/useSticky';
|
||||||
import FixedHolder from './FixedHolder';
|
import FixedHolder from './FixedHolder';
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties, Ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
@ -63,7 +63,7 @@ const EMPTY_SCROLL_TARGET = {};
|
||||||
|
|
||||||
export const INTERNAL_HOOKS = 'rc-table-internal-hook';
|
export const INTERNAL_HOOKS = 'rc-table-internal-hook';
|
||||||
|
|
||||||
export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<RecordType> {
|
export interface TableProps<RecordType = unknown> {
|
||||||
prefixCls?: string;
|
prefixCls?: string;
|
||||||
data?: RecordType[];
|
data?: RecordType[];
|
||||||
columns?: ColumnsType<RecordType>;
|
columns?: ColumnsType<RecordType>;
|
||||||
|
@ -73,10 +73,6 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
|
||||||
// Fixed Columns
|
// Fixed Columns
|
||||||
scroll?: { x?: number | true | string; y?: number | string };
|
scroll?: { x?: number | true | string; y?: number | string };
|
||||||
|
|
||||||
// Expandable
|
|
||||||
/** Config expand rows */
|
|
||||||
expandable?: ExpandableConfig<RecordType>;
|
|
||||||
indentSize?: number;
|
|
||||||
rowClassName?: string | RowClassName<RecordType>;
|
rowClassName?: string | RowClassName<RecordType>;
|
||||||
|
|
||||||
// Additional Part
|
// Additional Part
|
||||||
|
@ -94,17 +90,94 @@ export interface TableProps<RecordType = unknown> extends LegacyExpandableProps<
|
||||||
|
|
||||||
direction?: 'ltr' | 'rtl';
|
direction?: 'ltr' | 'rtl';
|
||||||
|
|
||||||
|
// Expandable
|
||||||
expandFixed?: boolean;
|
expandFixed?: boolean;
|
||||||
expandColumnWidth?: number;
|
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;
|
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;
|
sticky?: boolean | TableSticky;
|
||||||
|
|
||||||
|
canExpandable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent<TableProps>({
|
export default defineComponent<TableProps>({
|
||||||
name: 'Table',
|
name: 'Table',
|
||||||
slots: ['title', 'footer', 'summary', 'emptyText'],
|
slots: ['title', 'footer', 'summary', 'emptyText'],
|
||||||
emits: ['expand', 'expandedRowsChange'],
|
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 }) {
|
setup(props, { slots, emit }) {
|
||||||
const mergedData = computed(() => props.data || EMPTY_DATA);
|
const mergedData = computed(() => props.data || EMPTY_DATA);
|
||||||
const hasData = computed(() => !!mergedData.value.length);
|
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
|
* Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor
|
||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
|
props.canExpandable ||
|
||||||
mergedData.value.some(
|
mergedData.value.some(
|
||||||
record => record && typeof record === 'object' && record[mergedChildrenColumnName.value],
|
record => record && typeof record === 'object' && record[mergedChildrenColumnName.value],
|
||||||
)
|
)
|
||||||
|
@ -203,16 +277,19 @@ export default defineComponent<TableProps>({
|
||||||
|
|
||||||
const componentWidth = ref(0);
|
const componentWidth = ref(0);
|
||||||
|
|
||||||
const [columns, flattenColumns] = useColumns({
|
const [columns, flattenColumns] = useColumns(
|
||||||
...toRefs(props),
|
{
|
||||||
|
...toRefs(props),
|
||||||
|
|
||||||
// children,
|
// children,
|
||||||
expandable: computed(() => !!props.expandedRowRender),
|
expandable: computed(() => !!props.expandedRowRender),
|
||||||
expandedKeys: mergedExpandedKeys,
|
expandedKeys: mergedExpandedKeys,
|
||||||
getRowKey,
|
getRowKey,
|
||||||
onTriggerExpand,
|
onTriggerExpand,
|
||||||
expandIcon: mergedExpandIcon,
|
expandIcon: mergedExpandIcon,
|
||||||
});
|
},
|
||||||
|
computed(() => (props.internalHooks === INTERNAL_HOOKS ? props.transformColumns : null)),
|
||||||
|
);
|
||||||
|
|
||||||
const columnContext = computed(() => ({
|
const columnContext = computed(() => ({
|
||||||
columns: columns.value,
|
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
|
// Table layout
|
||||||
const mergedTableLayout = computed(() => {
|
const mergedTableLayout = computed(() => {
|
||||||
if (props.tableLayout) {
|
if (props.tableLayout) {
|
||||||
|
|
|
@ -103,37 +103,39 @@ function revertForRtl<RecordType>(columns: ColumnsType<RecordType>): ColumnsType
|
||||||
/**
|
/**
|
||||||
* Parse `columns` & `children` into `columns`.
|
* Parse `columns` & `children` into `columns`.
|
||||||
*/
|
*/
|
||||||
function useColumns<RecordType>({
|
function useColumns<RecordType>(
|
||||||
prefixCls,
|
{
|
||||||
columns: baseColumns,
|
prefixCls,
|
||||||
// children,
|
columns: baseColumns,
|
||||||
expandable,
|
// children,
|
||||||
expandedKeys,
|
expandable,
|
||||||
getRowKey,
|
expandedKeys,
|
||||||
onTriggerExpand,
|
getRowKey,
|
||||||
expandIcon,
|
onTriggerExpand,
|
||||||
rowExpandable,
|
expandIcon,
|
||||||
expandIconColumnIndex,
|
rowExpandable,
|
||||||
direction,
|
expandIconColumnIndex,
|
||||||
expandRowByClick,
|
direction,
|
||||||
expandColumnWidth,
|
expandRowByClick,
|
||||||
expandFixed,
|
expandColumnWidth,
|
||||||
}: {
|
expandFixed,
|
||||||
prefixCls?: Ref<string>;
|
}: {
|
||||||
columns?: Ref<ColumnsType<RecordType>>;
|
prefixCls?: Ref<string>;
|
||||||
expandable: Ref<boolean>;
|
columns?: Ref<ColumnsType<RecordType>>;
|
||||||
expandedKeys: Ref<Set<Key>>;
|
expandable: Ref<boolean>;
|
||||||
getRowKey: Ref<GetRowKey<RecordType>>;
|
expandedKeys: Ref<Set<Key>>;
|
||||||
onTriggerExpand: TriggerEventHandler<RecordType>;
|
getRowKey: Ref<GetRowKey<RecordType>>;
|
||||||
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
|
onTriggerExpand: TriggerEventHandler<RecordType>;
|
||||||
rowExpandable?: Ref<(record: RecordType) => boolean>;
|
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
|
||||||
expandIconColumnIndex?: Ref<number>;
|
rowExpandable?: Ref<(record: RecordType) => boolean>;
|
||||||
direction?: Ref<'ltr' | 'rtl'>;
|
expandIconColumnIndex?: Ref<number>;
|
||||||
expandRowByClick?: Ref<boolean>;
|
direction?: Ref<'ltr' | 'rtl'>;
|
||||||
expandColumnWidth?: Ref<number | string>;
|
expandRowByClick?: Ref<boolean>;
|
||||||
expandFixed?: Ref<FixedType>;
|
expandColumnWidth?: Ref<number | string>;
|
||||||
}): // transformColumns: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>,
|
expandFixed?: Ref<FixedType>;
|
||||||
[ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
|
},
|
||||||
|
transformColumns: Ref<(columns: ColumnsType<RecordType>) => ColumnsType<RecordType>>,
|
||||||
|
): [ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
|
||||||
// Add expand column
|
// Add expand column
|
||||||
const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
|
const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
|
||||||
if (expandable.value) {
|
if (expandable.value) {
|
||||||
|
@ -196,9 +198,9 @@ function useColumns<RecordType>({
|
||||||
|
|
||||||
const mergedColumns = computed(() => {
|
const mergedColumns = computed(() => {
|
||||||
let finalColumns = withExpandColumns.value;
|
let finalColumns = withExpandColumns.value;
|
||||||
// if (transformColumns) {
|
if (transformColumns.value) {
|
||||||
// finalColumns = transformColumns(finalColumns);
|
finalColumns = transformColumns.value(finalColumns);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Always provides at least one column for table display
|
// Always provides at least one column for table display
|
||||||
if (!finalColumns.length) {
|
if (!finalColumns.length) {
|
||||||
|
|
Loading…
Reference in New Issue