diff --git a/components/checkbox/Checkbox.tsx b/components/checkbox/Checkbox.tsx index 6ae6f8de2..885e53e36 100644 --- a/components/checkbox/Checkbox.tsx +++ b/components/checkbox/Checkbox.tsx @@ -1,4 +1,4 @@ -import { defineComponent, inject, nextTick } from 'vue'; +import { defineComponent, ExtractPropTypes, inject, nextTick } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; import VcCheckbox from '../vc-checkbox'; @@ -9,11 +9,8 @@ import type { RadioChangeEvent } from '../radio/interface'; import type { EventHandler } from '../_util/EventInterface'; function noop() {} -export default defineComponent({ - name: 'ACheckbox', - inheritAttrs: false, - __ANT_CHECKBOX: true, - props: { +export const checkboxProps = () => { + return { prefixCls: PropTypes.string, defaultChecked: PropTypes.looseBool, checked: PropTypes.looseBool, @@ -27,7 +24,17 @@ export default defineComponent({ autofocus: PropTypes.looseBool, onChange: PropTypes.func, 'onUpdate:checked': PropTypes.func, - }, + skipGroup: PropTypes.looseBool, + }; +}; + +export type CheckboxProps = Partial>>; + +export default defineComponent({ + name: 'ACheckbox', + inheritAttrs: false, + __ANT_CHECKBOX: true, + props: checkboxProps(), emits: ['change', 'update:checked'], setup() { return { @@ -38,6 +45,9 @@ export default defineComponent({ watch: { value(value, prevValue) { + if (this.skipGroup) { + return; + } nextTick(() => { const { checkboxGroupContext: checkboxGroup = {} } = this; if (checkboxGroup.registerValue && checkboxGroup.cancelValue) { @@ -85,7 +95,7 @@ export default defineComponent({ const props = getOptionProps(this); const { checkboxGroupContext: checkboxGroup, $attrs } = this; const children = getSlot(this); - const { indeterminate, prefixCls: customizePrefixCls, ...restProps } = props; + const { indeterminate, prefixCls: customizePrefixCls, skipGroup, ...restProps } = props; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('checkbox', customizePrefixCls); const { @@ -101,7 +111,7 @@ export default defineComponent({ prefixCls, ...restAttrs, }; - if (checkboxGroup) { + if (checkboxGroup && !skipGroup) { checkboxProps.onChange = (...args) => { this.$emit('change', ...args); checkboxGroup.toggleOption({ label: children, value: props.value }); diff --git a/components/checkbox/index.ts b/components/checkbox/index.ts index d28493db3..8fcf4b3b2 100644 --- a/components/checkbox/index.ts +++ b/components/checkbox/index.ts @@ -1,6 +1,7 @@ import type { App, Plugin } from 'vue'; -import Checkbox from './Checkbox'; +import Checkbox, { checkboxProps } from './Checkbox'; import CheckboxGroup from './Group'; +export type { CheckboxProps } from './Checkbox'; Checkbox.Group = CheckboxGroup; @@ -10,7 +11,7 @@ Checkbox.install = function (app: App) { app.component(CheckboxGroup.name, CheckboxGroup); return app; }; -export { CheckboxGroup }; +export { CheckboxGroup, checkboxProps }; export default Checkbox as typeof Checkbox & Plugin & { readonly Group: typeof CheckboxGroup; diff --git a/components/legacy-table/src/TableRow.jsx b/components/legacy-table/src/TableRow.jsx index bb8c38c70..04ee479bd 100644 --- a/components/legacy-table/src/TableRow.jsx +++ b/components/legacy-table/src/TableRow.jsx @@ -238,11 +238,6 @@ const TableRow = { for (let i = 0; i < columns.length; i += 1) { const column = columns[i]; - warning( - column.onCellClick === undefined, - 'column[onCellClick] is deprecated, please use column[customCell] instead.', - ); - cells.push( ; + Table?: TableLocale; Popconfirm?: Record; Upload?: Record; Form?: { diff --git a/components/style/themes/default.less b/components/style/themes/default.less index bc42cfe4a..41d2c4932 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -556,8 +556,8 @@ @table-header-bg: @background-color-light; @table-header-color: @heading-color; @table-header-sort-bg: @background-color-base; -@table-body-sort-bg: rgba(0, 0, 0, 0.01); -@table-row-hover-bg: @primary-1; +@table-body-sort-bg: #fafafa; +@table-row-hover-bg: @background-color-light; @table-selected-row-color: inherit; @table-selected-row-bg: @primary-1; @table-body-selected-sort-bg: @table-selected-row-bg; @@ -565,15 +565,31 @@ @table-expanded-row-bg: #fbfbfb; @table-padding-vertical: 16px; @table-padding-horizontal: 16px; +@table-padding-vertical-md: (@table-padding-vertical * 3 / 4); +@table-padding-horizontal-md: (@table-padding-horizontal / 2); +@table-padding-vertical-sm: (@table-padding-vertical / 2); +@table-padding-horizontal-sm: (@table-padding-horizontal / 2); +@table-border-color: @border-color-split; @table-border-radius-base: @border-radius-base; @table-footer-bg: @background-color-light; @table-footer-color: @heading-color; -@table-header-bg-sm: transparent; +@table-header-bg-sm: @table-header-bg; +@table-font-size: @font-size-base; +@table-font-size-md: @table-font-size; +@table-font-size-sm: @table-font-size; +@table-header-cell-split-color: rgba(0, 0, 0, 0.06); // Sorter // Legacy: `table-header-sort-active-bg` is used for hover not real active -@table-header-sort-active-bg: darken(@table-header-bg, 3%); +@table-header-sort-active-bg: rgba(0, 0, 0, 0.04); // Filter -@table-header-filter-active-bg: darken(@table-header-sort-active-bg, 5%); +@table-header-filter-active-bg: rgba(0, 0, 0, 0.04); +@table-filter-btns-bg: inherit; +@table-filter-dropdown-bg: @component-background; +@table-expand-icon-bg: @component-background; +@table-selection-column-width: 32px; +// Sticky +@table-sticky-scroll-bar-bg: fade(#000, 35%); +@table-sticky-scroll-bar-radius: 4px; // Tag // -- diff --git a/components/table/Column.tsx b/components/table/Column.tsx index a54fc2fcc..a478812ed 100644 --- a/components/table/Column.tsx +++ b/components/table/Column.tsx @@ -1,9 +1,10 @@ import { defineComponent } from 'vue'; -import { columnProps } from './interface'; +import { ColumnType } from './interface'; -export default defineComponent({ +export type ColumnProps = ColumnType; +export default defineComponent({ name: 'ATableColumn', - props: columnProps, + slots: ['title', 'filterIcon'], render() { return null; }, diff --git a/components/table/ColumnGroup.tsx b/components/table/ColumnGroup.tsx index 484d86aea..956f26992 100644 --- a/components/table/ColumnGroup.tsx +++ b/components/table/ColumnGroup.tsx @@ -1,15 +1,9 @@ import { defineComponent } from 'vue'; -import PropTypes, { withUndefined } from '../_util/vue-types'; -import { tuple } from '../_util/type'; +import { ColumnGroupProps } from '../vc-table/sugar/ColumnGroup'; -export default defineComponent({ +export default defineComponent>({ name: 'ATableColumnGroup', - props: { - fixed: withUndefined( - PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('left', 'right'))]), - ), - title: PropTypes.any, - }, + slots: ['title'], __ANT_TABLE_COLUMN_GROUP: true, render() { return null; diff --git a/components/table/ExpandIcon.tsx b/components/table/ExpandIcon.tsx new file mode 100644 index 000000000..3cdf015ff --- /dev/null +++ b/components/table/ExpandIcon.tsx @@ -0,0 +1,40 @@ +import classNames from '../_util/classNames'; +import { TableLocale } from './interface'; + +interface DefaultExpandIconProps { + prefixCls: string; + onExpand: (record: RecordType, e: MouseEvent) => void; + record: RecordType; + expanded: boolean; + expandable: boolean; +} + +function renderExpandIcon(locale: TableLocale) { + return function expandIcon({ + prefixCls, + onExpand, + record, + expanded, + expandable, + }: DefaultExpandIconProps) { + const iconPrefix = `${prefixCls}-row-expand-icon`; + + return ( + + + + + ); + } + + const menu = ( + + {dropdownContent} + + ); + + let filterIcon; + if (typeof column.filterIcon === 'function') { + filterIcon = column.filterIcon(filtered.value); + } else if (column.filterIcon) { + filterIcon = column.filterIcon; + } else { + filterIcon = ; + } + + return ( +
+ {slots.defalut?.()} + + { + e.stopPropagation(); + }} + > + {filterIcon} + + +
+ ); + }; + }, +}); diff --git a/components/table/hooks/useFilter/FilterWrapper.tsx b/components/table/hooks/useFilter/FilterWrapper.tsx new file mode 100644 index 000000000..7819b8ab1 --- /dev/null +++ b/components/table/hooks/useFilter/FilterWrapper.tsx @@ -0,0 +1,5 @@ +const FilterDropdownMenuWrapper = (_props, { slots }) => ( +
e.stopPropagation()}>{slots.default?.()}
+); + +export default FilterDropdownMenuWrapper; diff --git a/components/table/hooks/useFilter/index.tsx b/components/table/hooks/useFilter/index.tsx new file mode 100644 index 000000000..7ba5b2d4c --- /dev/null +++ b/components/table/hooks/useFilter/index.tsx @@ -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 { + column: ColumnType; + key: Key; + filteredKeys?: FilterKey; + forceFiltered?: boolean; +} + +function collectFilterStates( + columns: ColumnsType, + init: boolean, + pos?: string, +): FilterState[] { + let filterStates: FilterState[] = []; + + (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( + prefixCls: string, + dropdownPrefixCls: string, + columns: ColumnsType, + filterStates: FilterState[], + triggerFilter: (filterState: FilterState) => void, + getPopupContainer: GetPopupContainer | undefined, + locale: TableLocale, + pos?: string, +): ColumnsType { + return columns.map((column, index) => { + const columnPos = getColumnPos(index, pos); + const { filterMultiple = true } = column as ColumnType; + + let newColumn: ColumnsType[number] = column; + + if (newColumn.filters || newColumn.filterDropdown) { + const columnKey = getColumnKey(newColumn, columnPos); + const filterState = filterStates.find(({ key }) => columnKey === key); + + newColumn = { + ...newColumn, + title: (renderProps: ColumnTitleProps) => ( + + {renderColumnTitle(column.title, renderProps)} + + ), + }; + } + + 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(filterStates: FilterState[]) { + const currentFilters: Record = {}; + + 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( + data: RecordType[], + filterStates: FilterState[], +) { + 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 { + prefixCls: Ref; + dropdownPrefixCls: Ref; + mergedColumns: Ref>; + locale: Ref; + onFilterChange: ( + filters: Record, + filterStates: FilterState[], + ) => void; + getPopupContainer?: Ref; +} + +function useFilter({ + prefixCls, + dropdownPrefixCls, + mergedColumns, + locale, + onFilterChange, + getPopupContainer, +}: FilterConfig): [ + TransformColumns, + Ref[]>, + Ref>, +] { + const [filterStates, setFilterStates] = useState[]>( + 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) => { + const newFilterStates = mergedFilterStates.value.filter(({ key }) => key !== filterState.key); + newFilterStates.push(filterState); + setFilterStates(newFilterStates); + onFilterChange(generateFilterInfo(newFilterStates), newFilterStates); + }; + + const transformColumns = (innerColumns: ColumnsType) => { + return injectFilter( + prefixCls.value, + dropdownPrefixCls.value, + innerColumns, + mergedFilterStates.value, + triggerFilter, + getPopupContainer.value, + locale.value, + ); + }; + return [transformColumns, mergedFilterStates, filters]; +} + +export default useFilter; diff --git a/components/table/hooks/useLazyKVMap.ts b/components/table/hooks/useLazyKVMap.ts new file mode 100644 index 000000000..017305799 --- /dev/null +++ b/components/table/hooks/useLazyKVMap.ts @@ -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 { + kvMap?: Map; +} + +export default function useLazyKVMap( + dataRef: Ref, + childrenColumnNameRef: Ref, + getRowKeyRef: Ref>, +) { + const mapCacheRef = ref>({}); + + watch( + [dataRef, childrenColumnNameRef, getRowKeyRef], + () => { + const kvMap = new Map(); + 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]; +} diff --git a/components/table/hooks/usePagination.ts b/components/table/hooks/usePagination.ts new file mode 100644 index 000000000..f2501ccc4 --- /dev/null +++ b/components/table/hooks/usePagination.ts @@ -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(...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, + paginationRef: Ref, + onChange: (current: number, pageSize: number) => void, +): [Ref, () => 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>(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, + ]; +} diff --git a/components/table/hooks/useSelection.tsx b/components/table/hooks/useSelection.tsx new file mode 100644 index 000000000..8c00cf82d --- /dev/null +++ b/components/table/hooks/useSelection.tsx @@ -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(column: ColumnsType[number]): FixedType | undefined { + return (column && column.fixed) as FixedType; +} + +interface UseSelectionConfig { + prefixCls: Ref; + pageData: Ref; + data: Ref; + getRowKey: Ref>; + getRecordByKey: (key: Key) => RecordType; + expandType: Ref; + childrenColumnName: Ref; + expandIconColumnIndex?: Ref; + locale: Ref; + getPopupContainer?: Ref; +} + +export type INTERNAL_SELECTION_ITEM = + | SelectionItem + | typeof SELECTION_ALL + | typeof SELECTION_INVERT + | typeof SELECTION_NONE; + +function flattenData( + 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((record as any)[childrenColumnName], childrenColumnName), + ]; + } + }); + + return list; +} + +export default function useSelection( + rowSelectionRef: Ref | undefined>, + configRef: UseSelectionConfig, +): [TransformColumns, Ref>] { + // ======================== Caches ======================== + const preserveRecordsRef = ref(new Map()); + + // ========================= 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>(); + 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 = (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>(() => { + 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(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(); + 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): ColumnsType => { + 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 = ( + + {mergedSelections.value.map((selection, index) => { + const { key, text, onSelect: onSelectionClick } = selection; + return ( + { + onSelectionClick?.(recordKeys); + }} + > + {text} + + ); + })} + + ); + customizeSelections = ( +
+ + + + + +
+ ); + } + + 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 && ( +
+ + {customizeSelections} +
+ ); + } + + // 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: ( + 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: ( + 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]; +} diff --git a/components/table/hooks/useSorter.tsx b/components/table/hooks/useSorter.tsx new file mode 100644 index 000000000..2d30e0651 --- /dev/null +++ b/components/table/hooks/useSorter.tsx @@ -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(column: ColumnType): number | false { + if (typeof column.sorter === 'object' && typeof column.sorter.multiple === 'number') { + return column.sorter.multiple; + } + return false; +} + +function getSortFunction( + sorter: ColumnType['sorter'], +): CompareFn | 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 { + column: ColumnType; + key: Key; + sortOrder: SortOrder | null; + multiplePriority: number | false; +} + +function collectSortStates( + columns: ColumnsType, + init: boolean, + pos?: string, +): SortState[] { + let sortStates: SortState[] = []; + + function pushState(column: ColumnsType[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).children) { + if ('sortOrder' in column) { + // Controlled + pushState(column, columnPos); + } + sortStates = [ + ...sortStates, + ...collectSortStates((column as ColumnGroupType).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( + prefixCls: string, + columns: ColumnsType, + sorterSates: SortState[], + triggerSorter: (sorterSates: SortState) => void, + defaultSortDirections: SortOrder[], + tableLocale?: TableLocale, + tableShowSorterTooltip?: boolean | TooltipProps, + pos?: string, +): ColumnsType { + return (columns || []).map((column, index) => { + const columnPos = getColumnPos(index, pos); + let newColumn: ColumnsType[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) && ( + + ); + const downNode = sortDirections.includes(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) => { + const renderSortTitle = ( +
+ + {renderColumnTitle(column.title, renderProps)} + + + + {upNode} + {downNode} + + +
+ ); + return showSorterTooltip ? ( + {renderSortTitle} + ) : ( + 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(sorterStates: SortState) { + const { column, sortOrder } = sorterStates; + return { column, order: sortOrder, field: column.dataIndex, columnKey: column.key }; +} + +function generateSorterInfo( + sorterStates: SortState[], +): SorterResult | SorterResult[] { + 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( + data: readonly RecordType[], + sortStates: SortState[], + 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(record => { + const subRecords = (record as any)[childrenColumnName]; + if (subRecords) { + return { + ...record, + [childrenColumnName]: getSortData(subRecords, sortStates, childrenColumnName), + }; + } + return record; + }); +} + +interface SorterConfig { + prefixCls: Ref; + mergedColumns: Ref>; + onSorterChange: ( + sorterResult: SorterResult | SorterResult[], + sortStates: SortState[], + ) => void; + sortDirections: Ref; + tableLocale?: Ref; + showSorterTooltip?: Ref; +} + +export default function useFilterSorter({ + prefixCls, + mergedColumns, + onSorterChange, + sortDirections, + tableLocale, + showSorterTooltip, +}: SorterConfig): [ + TransformColumns, + Ref[]>, + Ref>, + Ref | SorterResult[]>, +] { + const [sortStates, setSortStates] = useState[]>( + 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[] = []; + + function patchStates(state: SortState) { + 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>(() => { + 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) { + 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) => + injectSorter( + prefixCls.value, + innerColumns, + mergedSorterStates.value, + triggerSorter, + sortDirections.value, + tableLocale.value, + showSorterTooltip.value, + ); + + const sorters = computed(() => generateSorterInfo(mergedSorterStates.value)); + + return [transformColumns, mergedSorterStates, columnTitleSorterProps, sorters]; +} diff --git a/components/table/hooks/useTitleColumns.tsx b/components/table/hooks/useTitleColumns.tsx new file mode 100644 index 000000000..4afc8d3fa --- /dev/null +++ b/components/table/hooks/useTitleColumns.tsx @@ -0,0 +1,29 @@ +import { Ref } from 'vue'; +import { TransformColumns, ColumnTitleProps, ColumnsType } from '../interface'; +import { renderColumnTitle } from '../util'; + +function fillTitle( + columns: ColumnsType, + columnTitleProps: ColumnTitleProps, +) { + 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( + columnTitleProps: Ref>, +): [TransformColumns] { + const filledColumns = (columns: ColumnsType) => + fillTitle(columns, columnTitleProps.value); + + return [filledColumns]; +} diff --git a/components/table/index.tsx b/components/table/index.tsx index dcce90144..17f423bac 100644 --- a/components/table/index.tsx +++ b/components/table/index.tsx @@ -1,98 +1,13 @@ -import type { App, Plugin } from 'vue'; -import { defineComponent } from 'vue'; -import T, { defaultTableProps } from './Table'; -import type Column from './Column'; -import type ColumnGroup from './ColumnGroup'; -import { - getOptionProps, - getKey, - getPropsData, - getSlot, - flattenChildren, -} from '../_util/props-util'; +import Table from './Table'; +import Column from './Column'; +import ColumnGroup from './ColumnGroup'; +import type { TableProps, TablePaginationConfig } from './Table'; +import { App } from 'vue'; + +export type { ColumnProps } from './Column'; +export type { ColumnsType, ColumnType, ColumnGroupType } from './interface'; +export type { TableProps, TablePaginationConfig }; -const Table = defineComponent({ - name: 'ATable', - Column: T.Column, - ColumnGroup: T.ColumnGroup, - inheritAttrs: false, - props: defaultTableProps, - methods: { - normalize(elements = []) { - const flattenElements = flattenChildren(elements); - const columns = []; - flattenElements.forEach(element => { - if (!element) { - return; - } - const key = getKey(element); - const style = element.props?.style || {}; - const cls = element.props?.class || ''; - const props = getPropsData(element); - const { default: children, ...restSlots } = element.children || {}; - const column = { ...restSlots, ...props, style, class: cls }; - if (key) { - column.key = key; - } - if (element.type?.__ANT_TABLE_COLUMN_GROUP) { - column.children = this.normalize(typeof children === 'function' ? children() : children); - } else { - const customRender = element.children?.default; - column.customRender = column.customRender || customRender; - } - columns.push(column); - }); - return columns; - }, - updateColumns(cols = []) { - const columns = []; - const { $slots } = this; - cols.forEach(col => { - const { slots = {}, ...restProps } = col; - const column = { - ...restProps, - }; - Object.keys(slots).forEach(key => { - const name = slots[key]; - if (column[key] === undefined && $slots[name]) { - column[key] = $slots[name]; - } - }); - // if (slotScopeName && $scopedSlots[slotScopeName]) { - // column.customRender = column.customRender || $scopedSlots[slotScopeName] - // } - if (col.children) { - column.children = this.updateColumns(column.children); - } - columns.push(column); - }); - return columns; - }, - }, - render() { - const { normalize, $slots } = this; - const props: any = { ...getOptionProps(this), ...this.$attrs }; - const columns = props.columns ? this.updateColumns(props.columns) : normalize(getSlot(this)); - let { title, footer } = props; - const { - title: slotTitle, - footer: slotFooter, - expandedRowRender = props.expandedRowRender, - expandIcon, - } = $slots; - title = title || slotTitle; - footer = footer || slotFooter; - const tProps = { - ...props, - columns, - title, - footer, - expandedRowRender, - expandIcon: this.$props.expandIcon || expandIcon, - }; - return ; - }, -}); /* istanbul ignore next */ Table.install = function (app: App) { app.component(Table.name, Table); diff --git a/components/table/index1.tsx b/components/table/index1.tsx new file mode 100644 index 000000000..dcce90144 --- /dev/null +++ b/components/table/index1.tsx @@ -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 ; + }, +}); +/* 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; + }; diff --git a/components/table/interface.tsx b/components/table/interface.tsx new file mode 100644 index 000000000..2e6e9bf77 --- /dev/null +++ b/components/table/interface.tsx @@ -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 = (a: T, b: T, sortOrder?: SortOrder) => number; + +export interface ColumnFilterItem { + text: VueNode; + value: string | number | boolean; + children?: ColumnFilterItem[]; +} + +export interface ColumnTitleProps { + /** @deprecated Please use `sorterColumns` instead. */ + sortOrder?: SortOrder; + /** @deprecated Please use `sorterColumns` instead. */ + sortColumn?: ColumnType; + sortColumns?: { column: ColumnType; order: SortOrder }[]; + + filters?: Record; +} + +export type ColumnTitle = VueNode | ((props: ColumnTitleProps) => 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 extends RcColumnType { + title?: ColumnTitle; + // Sorter + sorter?: + | boolean + | CompareFn + | { + compare?: CompareFn; + /** 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 extends Omit, 'dataIndex'> { + children: ColumnsType; +} + +export type ColumnsType = ( + | ColumnGroupType + | ColumnType +)[]; + +export interface SelectionItem { + key: string; + text: VueNode; + onSelect?: SelectionItemSelectFn; +} + +export type SelectionSelectFn = ( + record: T, + selected: boolean, + selectedRows: T[], + nativeEvent: Event, +) => void; + +export interface TableRowSelection { + /** 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>; + onSelect?: SelectionSelectFn; + 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; +} + +export type TransformColumns = ( + columns: ColumnsType, +) => ColumnsType; + +export interface TableCurrentDataSource { + currentDataSource: RecordType[]; + action: TableAction; +} + +export interface SorterResult { + column?: ColumnType; + 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[]; +} diff --git a/components/table/interface.ts b/components/table/interface1.ts similarity index 98% rename from components/table/interface.ts rename to components/table/interface1.ts index 7f8a2ac77..c068850df 100644 --- a/components/table/interface.ts +++ b/components/table/interface1.ts @@ -14,7 +14,7 @@ export const ColumnFilterItem = PropTypes.shape({ }).loose; export const columnProps = { - title: PropTypes.VNodeChild, + title: PropTypes.any, key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), dataIndex: PropTypes.string, customRender: PropTypes.func, @@ -49,7 +49,6 @@ export const columnProps = { ), sortDirections: PropTypes.array, // children?: ColumnProps[]; - // onCellClick?: (record: T, event: any) => void; // onCell?: (record: T) => any; // onHeaderCell?: (props: ColumnProps) => any; }; diff --git a/components/table/style/bordered.less b/components/table/style/bordered.less new file mode 100644 index 000000000..814560304 --- /dev/null +++ b/components/table/style/bordered.less @@ -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; + } +} diff --git a/components/table/style/index.less b/components/table/style/index.less index ea2f31c6a..42d8f0f9f 100644 --- a/components/table/style/index.less +++ b/components/table/style/index.less @@ -1,31 +1,28 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; +@import './size'; +@import './bordered'; @table-prefix-cls: ~'@{ant-prefix}-table'; +@dropdown-prefix-cls: ~'@{ant-prefix}-dropdown'; +@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions'; @table-header-icon-color: #bfbfbf; -@table-selection-column-width: 60px; +@table-header-icon-color-hover: darken(@table-header-icon-color, 10%); +@table-sticky-zindex: (@zindex-table-fixed + 1); +@table-sticky-scroll-bar-active-bg: fade(@table-sticky-scroll-bar-bg, 80%); .@{table-prefix-cls}-wrapper { + clear: both; + max-width: 100%; .clearfix(); } .@{table-prefix-cls} { .reset-component(); - position: relative; - clear: both; + font-size: @table-font-size; background: @table-bg; - - &-body { - transition: opacity 0.3s; - } - - &-empty &-body { - // https://github.com/ant-design/ant-design/issues/11135 - overflow-x: auto !important; - // https://github.com/ant-design/ant-design/issues/17175 - overflow-y: hidden !important; - } + border-radius: @table-border-radius-base; // https://github.com/ant-design/ant-design/issues/17611 table { @@ -36,727 +33,355 @@ border-spacing: 0; } - &-layout-fixed table { - table-layout: fixed; - } - - &-thead > tr > th { - color: @table-header-color; - font-weight: 500; - text-align: left; - background: @table-header-bg; - border-bottom: @border-width-base @border-style-base @border-color-split; - transition: background 0.3s ease; - - &[colspan]:not([colspan='1']) { - text-align: center; - } - - .@{iconfont-css-prefix}-filter, - .@{table-prefix-cls}-filter-icon { - position: absolute; - top: 0; - right: 0; - width: 28px; - height: 100%; - color: @table-header-icon-color; - font-size: @font-size-sm; - text-align: center; - cursor: pointer; - transition: all 0.3s; - - > svg { - position: absolute; - top: 50%; - left: 50%; - margin-top: (-@font-size-sm / 2) + 1px; - margin-left: (-@font-size-sm / 2); - } - } - - .@{table-prefix-cls}-filter-selected.@{iconfont-css-prefix} { - color: @primary-color; - } - - .@{table-prefix-cls}-column-sorter { - display: table-cell; - vertical-align: middle; - - .@{table-prefix-cls}-column-sorter-inner { - height: 1em; - margin-top: 0.35em; - margin-left: 0.57142857em; - color: @table-header-icon-color; - line-height: 1em; - text-align: center; - transition: all 0.3s; - - .@{table-prefix-cls}-column-sorter-up, - .@{table-prefix-cls}-column-sorter-down { - .iconfont-size-under-12px(11px); - - display: block; - height: 1em; - line-height: 1em; - transition: all 0.3s; - &.on { - color: @primary-color; - } - } - - &-full { - margin-top: -0.15em; - - .@{table-prefix-cls}-column-sorter-up, - .@{table-prefix-cls}-column-sorter-down { - height: 0.5em; - line-height: 0.5em; - } - - .@{table-prefix-cls}-column-sorter-down { - margin-top: 0.125em; - } - } - } - } - - &.@{table-prefix-cls}-column-has-actions { - position: relative; - background-clip: padding-box; // For Firefox background bug, https://github.com/ant-design/ant-design/issues/12628 - /* stylelint-disable-next-line */ - -webkit-background-clip: border-box; // For Chrome extra space: https://github.com/ant-design/ant-design/issues/14926 - - &.@{table-prefix-cls}-column-has-filters { - // https://github.com/ant-design/ant-design/issues/12650 - padding-right: 30px !important; - - .@{iconfont-css-prefix}-filter, - .@{table-prefix-cls}-filter-icon { - &.@{table-prefix-cls}-filter-open { - color: @text-color-secondary; - background: @table-header-filter-active-bg; - } - } - // Very complicated styles logic but necessary - &:hover { - .@{iconfont-css-prefix}-filter, - .@{table-prefix-cls}-filter-icon { - &:hover { - color: @text-color-secondary; - background: @table-header-filter-active-bg; - } - &:active { - color: @text-color; - } - } - } - } - - &.@{table-prefix-cls}-column-has-sorters { - cursor: pointer; - &:hover { - background: @table-header-sort-active-bg; - .@{iconfont-css-prefix}-filter, - .@{table-prefix-cls}-filter-icon { - background: @table-header-sort-active-bg; - } - } - &:active { - .@{table-prefix-cls}-column-sorter-up:not(.on), - .@{table-prefix-cls}-column-sorter-down:not(.on) { - color: @text-color-secondary; - } - } - } - } - - .@{table-prefix-cls}-header-column { - display: inline-block; - max-width: 100%; - vertical-align: top; - - .@{table-prefix-cls}-column-sorters { - display: table; - - > .@{table-prefix-cls}-column-title { - display: table-cell; - vertical-align: middle; - } - - > *:not(.@{table-prefix-cls}-column-sorter) { - position: relative; - } - &::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: transparent; - transition: all 0.3s; - content: ''; - } - &:hover::before { - background: rgba(0, 0, 0, 0.04); - } - } - } - - &.@{table-prefix-cls}-column-has-sorters { - user-select: none; - } - } - - &-thead > tr:first-child > th { - &:first-child { - border-top-left-radius: @table-border-radius-base; - } - - &:last-child { - border-top-right-radius: @table-border-radius-base; - } - } - - &-thead > tr:not(:last-child) > th { - &[colspan] { - border-bottom: 0; - } - } - - &-tbody > tr > td { - border-bottom: @border-width-base @border-style-base @border-color-split; - transition: background 0.3s; - } - - &-thead > tr, - &-tbody > tr { - &.@{table-prefix-cls}-row-hover, - &:hover { - &:not(.@{table-prefix-cls}-expanded-row):not(.@{table-prefix-cls}-row-selected) > td { - background: @table-row-hover-bg; - } - } - &.@{table-prefix-cls}-row-selected > td { - &.@{table-prefix-cls}-column-sort { - background: @table-body-selected-sort-bg; - } - } - &:hover { - &.@{table-prefix-cls}-row-selected > td { - background: @table-selected-row-hover-bg; - &.@{table-prefix-cls}-column-sort { - background: @table-body-selected-sort-bg; - } - } - } - } - - &-thead > tr:hover { - background: none; - } - - &-footer { - position: relative; - padding: @table-padding-vertical @table-padding-horizontal; - color: @table-footer-color; - background: @table-footer-bg; - border-top: @border-width-base @border-style-base @border-color-split; - border-radius: 0 0 @table-border-radius-base @table-border-radius-base; - &::before { - position: absolute; - top: -1px; - left: 0; - width: 100%; - height: 1px; - background: @table-footer-bg; - content: ''; - } - } - - &.@{table-prefix-cls}-bordered &-footer { - border: @border-width-base @border-style-base @border-color-split; - } - - &-title { - position: relative; - top: 1px; - padding: @table-padding-vertical 0; - border-radius: @table-border-radius-base @table-border-radius-base 0 0; - } - - &.@{table-prefix-cls}-bordered &-title { - padding-right: @table-padding-horizontal; - padding-left: @table-padding-horizontal; - border: @border-width-base @border-style-base @border-color-split; - } - - &-title + &-content { - position: relative; - border-radius: @table-border-radius-base @table-border-radius-base 0 0; - - .@{table-prefix-cls}-bordered & { - &, - table, - .@{table-prefix-cls}-thead > tr:first-child > th { - border-radius: 0; - } - } - } - - // https://github.com/ant-design/ant-design/issues/4373 - &-without-column-header &-title + &-content, - &-without-column-header table { - border-radius: 0; - } - - // https://github.com/ant-design/ant-design/issues/14834 - &-without-column-header&-bordered&-empty &-placeholder { - border-top: 1px solid @border-color-split; - border-radius: @border-radius-base; - } - - &-tbody > tr.@{table-prefix-cls}-row-selected td { - color: @table-selected-row-color; - background: @table-selected-row-bg; - } - - &-thead > tr > th.@{table-prefix-cls}-column-sort { - background: @table-header-sort-bg; - } - - &-tbody > tr > td.@{table-prefix-cls}-column-sort { - background: @table-body-sort-bg; - } - + // ============================= Cell ============================= &-thead > tr > th, - &-tbody > tr > td { + &-tbody > tr > td, + tfoot > tr > th, + tfoot > tr > td { + position: relative; padding: @table-padding-vertical @table-padding-horizontal; overflow-wrap: break-word; } - &-expand-icon-th, - &-row-expand-icon-cell { - width: 50px; - min-width: 50px; - text-align: center; - } - - &-header { + &-cell-ellipsis { overflow: hidden; - background: @table-header-bg; - } + white-space: nowrap; + text-overflow: ellipsis; + word-break: keep-all; - &-header table { - border-radius: @table-border-radius-base @table-border-radius-base 0 0; - } + // Fixed first or last should special process + &.@{table-prefix-cls}-cell-fix-left-last, + &.@{table-prefix-cls}-cell-fix-right-first { + overflow: visible; - &-loading { - position: relative; - .@{table-prefix-cls}-body { - background: @component-background; - opacity: 0.5; - } - .@{table-prefix-cls}-spin-holder { - position: absolute; - top: 50%; - left: 50%; - height: 20px; - margin-left: -30px; - line-height: 20px; - } - .@{table-prefix-cls}-with-pagination { - margin-top: -20px; - } - .@{table-prefix-cls}-without-pagination { - margin-top: 10px; - } - } - - &-bordered { - .@{table-prefix-cls}-header > table, - .@{table-prefix-cls}-body > table, - .@{table-prefix-cls}-fixed-left table, - .@{table-prefix-cls}-fixed-right table { - border: @border-width-base @border-style-base @border-color-split; - border-right: 0; - border-bottom: 0; - } - - &.@{table-prefix-cls}-empty { - .@{table-prefix-cls}-placeholder { - border-right: @border-width-base @border-style-base @border-color-split; - border-left: @border-width-base @border-style-base @border-color-split; + .@{table-prefix-cls}-cell-content { + display: block; + overflow: hidden; + text-overflow: ellipsis; } } - &.@{table-prefix-cls}-fixed-header { - .@{table-prefix-cls}-header > table { - border-bottom: 0; - } - - .@{table-prefix-cls}-body > table { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - .@{table-prefix-cls}-header + .@{table-prefix-cls}-body > table, - .@{table-prefix-cls}-body-inner > table { - border-top: 0; - } - } - - .@{table-prefix-cls}-thead > tr:not(:last-child) > th { - border-bottom: @border-width-base @border-style-base @border-color-split; - } - - .@{table-prefix-cls}-thead > tr > th, - .@{table-prefix-cls}-tbody > tr > td { - border-right: @border-width-base @border-style-base @border-color-split; + .@{table-prefix-cls}-column-title { + overflow: hidden; + text-overflow: ellipsis; + word-break: keep-all; } } - &-placeholder { - position: relative; - z-index: 1; - margin-top: -1px; + // ============================ Title ============================= + &-title { padding: @table-padding-vertical @table-padding-horizontal; - color: @disabled-color; - font-size: @font-size-base; - text-align: center; - background: @component-background; - border-top: @border-width-base @border-style-base @border-color-split; - border-bottom: @border-width-base @border-style-base @border-color-split; - border-radius: 0 0 @border-radius-base @border-radius-base; } - &-pagination.@{ant-prefix}-pagination { - float: right; - margin: 16px 0; + // ============================ Footer ============================ + &-footer { + padding: @table-padding-vertical @table-padding-horizontal; + color: @table-footer-color; + background: @table-footer-bg; } - &-filter-dropdown { - position: relative; - min-width: 96px; - margin-left: -8px; - background: @component-background; - border-radius: @border-radius-base; - box-shadow: @box-shadow-base; + // ============================ Header ============================ + &-thead { + > tr { + > th { + position: relative; + color: @table-header-color; + font-weight: 500; + text-align: left; + background: @table-header-bg; + border-bottom: @border-width-base @border-style-base @table-border-color; + transition: background 0.3s ease; - .@{ant-prefix}-dropdown-menu { - // https://github.com/ant-design/ant-design/issues/4916 - // https://github.com/ant-design/ant-design/issues/19542 - max-height: ~'calc(100vh - 130px)'; - overflow-x: hidden; - border: 0; - border-radius: @border-radius-base @border-radius-base 0 0; - box-shadow: none; + &[colspan]:not([colspan='1']) { + text-align: center; + } - &-item > label + span { - padding-right: 0; - } - - &-sub { - border-radius: @border-radius-base; - box-shadow: @box-shadow-base; - } - - .@{ant-prefix}-dropdown-submenu-contain-selected { - .@{ant-prefix}-dropdown-menu-submenu-title::after { - color: @primary-color; - font-weight: bold; - text-shadow: 0 0 2px @primary-2; + &:not(:last-child):not(.@{table-prefix-cls}-selection-column):not(.@{table-prefix-cls}-row-expand-icon-cell):not([colspan])::before { + position: absolute; + top: 50%; + right: 0; + width: 1px; + height: 1.6em; + background-color: @table-header-cell-split-color; + transform: translateY(-50%); + transition: background-color 0.3s; + content: ''; } } } - .@{ant-prefix}-dropdown-menu-item { - overflow: hidden; - } - - > .@{ant-prefix}-dropdown-menu > .@{ant-prefix}-dropdown-menu-item:last-child, - > .@{ant-prefix}-dropdown-menu - > .@{ant-prefix}-dropdown-menu-submenu:last-child - .@{ant-prefix}-dropdown-menu-submenu-title { - border-radius: 0; - } - - &-btns { - padding: 7px 8px; - overflow: hidden; - border-top: @border-width-base @border-style-base @border-color-split; - } - - &-link { - color: @link-color; - &:hover { - color: @link-hover-color; - } - &:active { - color: @link-active-color; - } - &.confirm { - float: left; - } - &.clear { - float: right; + > tr:not(:last-child) > th { + &[colspan] { + border-bottom: 0; } } } - &-selection { - white-space: nowrap; + // ============================= Body ============================= + &-tbody { + > tr { + > td { + border-bottom: @border-width-base @border-style-base @table-border-color; + transition: background 0.3s; - &-select-all-custom { - margin-right: 4px !important; - } + // ========================= Nest Table =========================== + > .@{table-prefix-cls}-wrapper:only-child, + > .@{table-prefix-cls}-expanded-row-fixed > .@{table-prefix-cls}-wrapper:only-child { + .@{table-prefix-cls} { + margin: -@table-padding-vertical -@table-padding-horizontal -@table-padding-vertical (@table-padding-horizontal + + ceil(@font-size-sm * 1.4)); - .@{iconfont-css-prefix}-down { - color: @table-header-icon-color; - transition: all 0.3s; - } + &-tbody > tr:last-child > td { + border-bottom: 0; - &-menu { - min-width: 96px; - margin-top: 5px; - margin-left: -30px; - background: @component-background; - border-radius: @border-radius-base; - box-shadow: @box-shadow-base; - - .@{ant-prefix}-action-down { - color: @table-header-icon-color; + &:first-child, + &:last-child { + border-radius: 0; + } + } + } + } } - } - &-down { - display: inline-block; - padding: 0; - line-height: 1; - cursor: pointer; - &:hover .@{iconfont-css-prefix}-down { - color: fade(@black, 60%); + &.@{table-prefix-cls}-row:hover { + > td { + background: @table-row-hover-bg; + } + } + + &.@{table-prefix-cls}-row-selected { + > td { + background: @table-selected-row-bg; + border-color: rgba(0, 0, 0, 0.03); + } + + &:hover { + > td { + background: @table-selected-row-hover-bg; + } + } } } } - &-row { - &-expand-icon { - .operation-unit(); + // =========================== Summary ============================ + &-summary { + background: @table-bg; - display: inline-block; - width: 17px; - height: 17px; - color: inherit; - line-height: 13px; - text-align: center; - background: @component-background; - border: @border-width-base @border-style-base @border-color-split; - border-radius: @border-radius-sm; - outline: none; - transition: all 0.3s; - user-select: none; + div& { + box-shadow: 0 -@border-width-base 0 @table-border-color; + } - &:focus, - &:hover, - &:active { - border-color: currentColor; + > tr { + > th, + > td { + border-bottom: @border-width-base @border-style-base @table-border-color; } } - - &-expanded::after { - content: '-'; - } - - &-collapsed::after { - content: '+'; - } - - &-spaced { - visibility: hidden; - &::after { - content: '.'; - } - } - - &-cell-ellipsis, - &-cell-ellipsis .@{table-prefix-cls}-column-title { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &-cell-ellipsis .@{table-prefix-cls}-column-title { - display: block; - } - - &-cell-break-word { - word-wrap: break-word; - word-break: break-word; - } } - tr&-expanded-row { - &, + // ========================== Pagination ========================== + &-pagination.@{ant-prefix}-pagination { + margin: 16px 0; + } + + &-pagination { + display: flex; + flex-wrap: wrap; + row-gap: @padding-xs; + + > * { + flex: none; + } + + &-left { + justify-content: flex-start; + } + + &-center { + justify-content: center; + } + + &-right { + justify-content: flex-end; + } + } + + // ================================================================ + // = Function = + // ================================================================ + + // ============================ Sorter ============================ + &-thead th.@{table-prefix-cls}-column-has-sorters { + cursor: pointer; + transition: all 0.3s; + &:hover { - background: @table-expanded-row-bg; + background: @table-header-sort-active-bg; + + &::before { + background-color: transparent !important; + } } - td > .@{table-prefix-cls}-wrapper { - margin: -@table-padding-vertical -@table-padding-horizontal -@table-padding-vertical - 1px; + // https://github.com/ant-design/ant-design/issues/30969 + &.@{table-prefix-cls}-cell-fix-left:hover, + &.@{table-prefix-cls}-cell-fix-right:hover { + background: lighten(@black, 96%); } } - .@{table-prefix-cls}-row-indent + .@{table-prefix-cls}-row-expand-icon { - margin-right: 8px; - } + &-thead th.@{table-prefix-cls}-column-sort { + background: @table-header-sort-bg; - &-scroll { - overflow: auto; - overflow-x: hidden; - table { - min-width: 100%; + &::before { + background-color: transparent !important; } } - &-body-inner { - height: 100%; + td&-column-sort { + background: @table-body-sort-bg; } - &-fixed-header > &-content > &-scroll > &-body { + &-column-title { position: relative; - background: @component-background; + z-index: 1; + flex: 1; } - &-fixed-header &-body-inner { - overflow: scroll; - } + &-column-sorters { + display: flex; + flex: auto; + align-items: center; + justify-content: space-between; - &-fixed-header &-scroll &-header { - margin-bottom: -20px; - padding-bottom: 20px; - overflow: scroll; - // Workaround for additional scroll bar on the table header - // https://github.com/ant-design/ant-design/issues/6515#issuecomment-419634369 - opacity: 0.9999; - - &::-webkit-scrollbar { - border: 1px solid @border-color-split; - border-width: 0 0 1px 0; + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + content: ''; } } - &-hide-scrollbar { - // https://github.com/ant-design/ant-design/issues/4637 - // https://stackoverflow.com/a/54101063 - // https://github.com/react-component/table/pull/333 - scrollbar-color: transparent transparent; - min-width: unset; + &-column-sorter { + color: @table-header-icon-color; + font-size: 0; + transition: color 0.3s; - &::-webkit-scrollbar { - // set min width to window chrome scrollbar - // https://github.com/ant-design/ant-design/issues/19952#issuecomment-559367149 - min-width: inherit; - background-color: transparent; + &-inner { + display: inline-flex; + flex-direction: column; + align-items: center; + } + + &-up, + &-down { + font-size: 11px; + + &.active { + color: @primary-color; + } + } + + &-up + &-down { + margin-top: -0.3em; } } - // optimize header style of borderd table after hide extra scrollbar - &-bordered&-fixed-header &-scroll &-header { - &::-webkit-scrollbar { - border: 1px solid @border-color-split; - border-width: 1px 1px 1px 0; + &-column-sorters:hover &-column-sorter { + color: darken(@table-header-icon-color, 10%); + } + + // ============================ Filter ============================ + &-filter-column { + display: flex; + justify-content: space-between; + } + + &-filter-trigger { + position: relative; + display: flex; + align-items: center; + margin: -4px (-@table-padding-horizontal / 2) -4px 4px; + padding: 0 4px; + color: @table-header-icon-color; + font-size: @font-size-sm; + border-radius: @border-radius-base; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: @text-color-secondary; + background: @table-header-filter-active-bg; } - &.@{table-prefix-cls}-hide-scrollbar - .@{table-prefix-cls}-thead - > tr:only-child - > th:last-child { - border-right-color: transparent; + + &.active { + color: @primary-color; } } - &-fixed-left, - &-fixed-right { - position: absolute; - top: 0; - z-index: @zindex-table-fixed; - overflow: hidden; - border-radius: 0; - transition: box-shadow 0.3s ease; - table { - width: auto; - background: @component-background; - } - } + // Dropdown + &-filter-dropdown { + .reset-component(); - &-fixed-header &-fixed-left &-body-outer &-fixed, - &-fixed-header &-fixed-right &-body-outer &-fixed { - border-radius: 0; - } + // Reset menu + .@{dropdown-prefix-cls}-menu { + // https://github.com/ant-design/ant-design/issues/4916 + // https://github.com/ant-design/ant-design/issues/19542 + max-height: 264px; + overflow-x: hidden; + border: 0; + box-shadow: none; + } - &-fixed-left { - left: 0; - box-shadow: 6px 0 6px -4px @shadow-color; - .@{table-prefix-cls}-header { - overflow-y: hidden; - } - // hide scrollbar in left fixed columns - .@{table-prefix-cls}-body-inner { - margin-right: -20px; - padding-right: 20px; - } - .@{table-prefix-cls}-fixed-header & .@{table-prefix-cls}-body-inner { - padding-right: 0; + min-width: 120px; + background-color: @table-filter-dropdown-bg; + + border-radius: @border-radius-base; + box-shadow: @box-shadow-base; + + &-submenu > ul { + max-height: calc(100vh - 130px); + overflow-x: hidden; + overflow-y: auto; } + + // Checkbox &, - table { - border-radius: @table-border-radius-base 0 0 0; + &-submenu { + .@{ant-prefix}-checkbox-wrapper + span { + padding-left: 8px; + } } - .@{table-prefix-cls}-thead > tr > th:last-child { - border-top-right-radius: 0; + + // Operation + &-btns { + display: flex; + justify-content: space-between; + padding: 7px 8px 7px 3px; + overflow: hidden; + background-color: @table-filter-btns-bg; + border-top: @border-width-base @border-style-base @table-border-color; } } - &-fixed-right { - right: 0; - box-shadow: -6px 0 6px -4px @shadow-color; - &, - table { - border-radius: 0 @table-border-radius-base 0 0; - } - // hide expand row content in right-fixed Table - // https://github.com/ant-design/ant-design/issues/1898 - .@{table-prefix-cls}-expanded-row { - color: transparent; - pointer-events: none; - } - .@{table-prefix-cls}-thead > tr > th:first-child { - border-top-left-radius: 0; - } + // ========================== Selections ========================== + &-selection-col { + width: @table-selection-column-width; } - &&-scroll-position-left &-fixed-left { - box-shadow: none; + &-bordered &-selection-col { + width: @table-selection-column-width + 18px; } - &&-scroll-position-right &-fixed-right { - box-shadow: none; - } - - // ========================== Row Selection ========================== - colgroup { - > col.@{table-prefix-cls}-selection-col { - width: @table-selection-column-width; - } - } - - &-thead > tr > th.@{table-prefix-cls}-selection-column-custom { - .@{table-prefix-cls}-selection { - margin-right: -15px; - } - } - - &-thead > tr > th.@{table-prefix-cls}-selection-column, - &-tbody > tr > td.@{table-prefix-cls}-selection-column { + table tr th&-selection-column, + table tr td&-selection-column { + padding-right: @padding-xs; + padding-left: @padding-xs; text-align: center; .@{ant-prefix}-radio-wrapper { @@ -764,8 +389,163 @@ } } - &-row[class*='@{table-prefix-cls}-row-level-0'] .@{table-prefix-cls}-selection-column > span { - display: inline-block; + table tr th&-selection-column::after { + background-color: transparent !important; + } + + &-selection { + position: relative; + display: inline-flex; + flex-direction: column; + + &-extra { + position: absolute; + top: 0; + z-index: 1; + cursor: pointer; + transition: all 0.3s; + margin-inline-start: 100%; + padding-inline-start: @padding-xss; + + .@{iconfont-css-prefix} { + color: @table-header-icon-color; + font-size: 10px; + + &:hover { + color: @table-header-icon-color-hover; + } + } + } + } + + // ========================== Expandable ========================== + &-expand-icon-col { + width: 48px; + } + + &-row-expand-icon-cell { + text-align: center; + } + + &-row-indent { + float: left; + height: 1px; + } + + &-row-expand-icon { + .operation-unit(); + position: relative; + display: inline-flex; + float: left; + box-sizing: border-box; + width: @expand-icon-size; + height: @expand-icon-size; + padding: 0; + color: inherit; + line-height: ceil(((@font-size-sm * 1.4 - @border-width-base * 3) / 2)) * 2 + @border-width-base * + 3; + background: @table-expand-icon-bg; + border: @border-width-base @border-style-base @table-border-color; + border-radius: @border-radius-base; + outline: none; + transform: scale((unit(@checkbox-size) / unit(@expand-icon-size))); + transition: all 0.3s; + user-select: none; + @expand-icon-size: ceil(((@font-size-sm * 1.4 - @border-width-base * 3) / 2)) * 2 + + @border-width-base * 3; + + &:focus, + &:hover, + &:active { + border-color: currentColor; + } + + &::before, + &::after { + position: absolute; + background: currentColor; + transition: transform 0.3s ease-out; + content: ''; + } + + &::before { + top: ceil(((@font-size-sm * 1.4 - @border-width-base * 3) / 2)); + right: 3px; + left: 3px; + height: @border-width-base; + } + + &::after { + top: 3px; + bottom: 3px; + left: ceil(((@font-size-sm * 1.4 - @border-width-base * 3) / 2)); + width: @border-width-base; + transform: rotate(90deg); + } + + // Motion effect + &-collapsed::before { + transform: rotate(-180deg); + } + &-collapsed::after { + transform: rotate(0deg); + } + + &-spaced { + &::before, + &::after { + display: none; + content: none; + } + background: transparent; + border: 0; + visibility: hidden; + } + + .@{table-prefix-cls}-row-indent + & { + margin-top: ((@font-size-base * @line-height-base - @border-width-base * 3) / 2) - + ceil(((@font-size-sm * 1.4 - @border-width-base * 3) / 2)); + margin-right: @padding-xs; + } + } + + tr&-expanded-row { + &, + &:hover { + > td { + background: @table-expanded-row-bg; + } + } + + // https://github.com/ant-design/ant-design/issues/25573 + .@{descriptions-prefix-cls}-view { + display: flex; + + table { + flex: auto; + width: auto; + } + } + } + + // With fixed + .@{table-prefix-cls}-expanded-row-fixed { + position: relative; + margin: -@table-padding-vertical -@table-padding-horizontal; + padding: @table-padding-vertical @table-padding-horizontal; + } + + // ========================= Placeholder ========================== + &-tbody > tr&-placeholder { + text-align: center; + .@{table-prefix-cls}-empty & { + color: @disabled-color; + } + &:hover { + > td { + background: @component-background; + } + } } // ============================ Fixed ============================= @@ -857,23 +637,52 @@ box-shadow: inset -10px 0 8px -8px darken(@shadow-color, 5%); } } -} - -.@{table-prefix-cls}-filter-dropdown, -.@{table-prefix-cls}-filter-dropdown-submenu { - .@{ant-prefix}-checkbox-wrapper + span { - padding-left: 8px; + &-sticky { + &-holder { + position: sticky; + z-index: @table-sticky-zindex; + } + &-scroll { + position: sticky; + bottom: 0; + z-index: @table-sticky-zindex; + display: flex; + align-items: center; + background: lighten(@table-border-color, 80%); + border-top: 1px solid @table-border-color; + opacity: 0.6; + &:hover { + transform-origin: center bottom; + } + &-bar { + height: 8px; + background-color: @table-sticky-scroll-bar-bg; + border-radius: @table-sticky-scroll-bar-radius; + &:hover { + background-color: @table-sticky-scroll-bar-active-bg; + } + &-active { + background-color: @table-sticky-scroll-bar-active-bg; + } + } + } } } -/** -* Another fix of Firefox: -*/ -@supports (-moz-appearance: meterbar) { - // https://github.com/ant-design/ant-design/issues/12628 - .@{table-prefix-cls}-thead > tr > th.@{table-prefix-cls}-column-has-actions { - background-clip: padding-box; +@media all and (-ms-high-contrast: none) { + .@{table-prefix-cls} { + &-ping-left { + .@{table-prefix-cls}-cell-fix-left-last::after { + box-shadow: none !important; + } + } + &-ping-right { + .@{table-prefix-cls}-cell-fix-right-first::after { + box-shadow: none !important; + } + } } } -@import './size'; +@import './radius'; +@import './rtl'; diff --git a/components/table/style/index.ts b/components/table/style/index.tsx similarity index 77% rename from components/table/style/index.ts rename to components/table/style/index.tsx index c1ff86a1a..e70861892 100644 --- a/components/table/style/index.ts +++ b/components/table/style/index.tsx @@ -3,9 +3,12 @@ import './index.less'; // style dependencies // deps-lint-skip: menu +// deps-lint-skip: grid +import '../../button/style'; import '../../empty/style'; import '../../radio/style'; import '../../checkbox/style'; import '../../dropdown/style'; import '../../spin/style'; import '../../pagination/style'; +import '../../tooltip/style'; diff --git a/components/table/style/radius.less b/components/table/style/radius.less new file mode 100644 index 000000000..1927c1a06 --- /dev/null +++ b/components/table/style/radius.less @@ -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; + } +} diff --git a/components/table/style/rtl.less b/components/table/style/rtl.less new file mode 100644 index 000000000..82758f291 --- /dev/null +++ b/components/table/style/rtl.less @@ -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); + } + } + } +} diff --git a/components/table/style/size.less b/components/table/style/size.less index 7772c99db..f34a63c13 100644 --- a/components/table/style/size.less +++ b/components/table/style/size.less @@ -1,38 +1,34 @@ -@table-padding-vertical-md: (@table-padding-vertical * 3 / 4); -@table-padding-horizontal-md: (@table-padding-horizontal / 2); -@table-padding-vertical-sm: (@table-padding-vertical / 2); -@table-padding-horizontal-sm: (@table-padding-horizontal / 2); +@import './index'; -.table-size(@size, @padding-vertical, @padding-horizontal) { +.table-size(@size, @padding-vertical, @padding-horizontal, @font-size) { .@{table-prefix-cls}.@{table-prefix-cls}-@{size} { - > .@{table-prefix-cls}-title, - > .@{table-prefix-cls}-content > .@{table-prefix-cls}-footer { + font-size: @font-size; + + .@{table-prefix-cls}-title, + .@{table-prefix-cls}-footer, + .@{table-prefix-cls}-thead > tr > th, + .@{table-prefix-cls}-tbody > tr > td, + tfoot > tr > th, + tfoot > tr > td { padding: @padding-vertical @padding-horizontal; } - > .@{table-prefix-cls}-content { - > .@{table-prefix-cls}-header > table, - > .@{table-prefix-cls}-body > table, - > .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table, - > .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table, - > .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table, - > .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table, - > .@{table-prefix-cls}-fixed-left - > .@{table-prefix-cls}-body-outer - > .@{table-prefix-cls}-body-inner - > table, - > .@{table-prefix-cls}-fixed-right - > .@{table-prefix-cls}-body-outer - > .@{table-prefix-cls}-body-inner - > table { - > .@{table-prefix-cls}-thead > tr > th, - > .@{table-prefix-cls}-tbody > tr > td { - padding: @padding-vertical @padding-horizontal; - } - } + + .@{table-prefix-cls}-filter-trigger { + margin-right: -(@padding-horizontal / 2); } - tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper { - margin: -@padding-vertical (-@table-padding-horizontal / 2) -@padding-vertical - 1px; + .@{table-prefix-cls}-expanded-row-fixed { + margin: -@padding-vertical -@padding-horizontal; + } + + .@{table-prefix-cls}-tbody { + // ========================= Nest Table =========================== + .@{table-prefix-cls}-wrapper:only-child { + .@{table-prefix-cls} { + margin: -@padding-vertical -@padding-horizontal -@padding-vertical (@padding-horizontal + + ceil((@font-size-sm * 1.4))); + } + } } } } @@ -40,14 +36,17 @@ // ================================================================ // = Middle = // ================================================================ -.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md); +.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md, @table-font-size-md); // ================================================================ // = Small = // ================================================================ -.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm); +.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm, @table-font-size-sm); .@{table-prefix-cls}-small { + .@{table-prefix-cls}-thead > tr > th { + background-color: @table-header-bg-sm; + } .@{table-prefix-cls}-selection-column { width: 46px; min-width: 46px; diff --git a/components/table/util.ts b/components/table/util.ts index efd8c858f..cdb423d20 100644 --- a/components/table/util.ts +++ b/components/table/util.ts @@ -1,73 +1,27 @@ -export function flatArray(data = [], childrenName = 'children') { - const result = []; - const loop = array => { - array.forEach(item => { - if (item[childrenName]) { - const newItem = { ...item }; - delete newItem[childrenName]; - result.push(newItem); - if (item[childrenName].length > 0) { - loop(item[childrenName]); - } - } else { - result.push(item); - } - }); - }; - loop(data); - return result; +import type { ColumnType, ColumnTitle, ColumnTitleProps, Key } from './interface'; + +export function getColumnKey(column: ColumnType, defaultKey: string): Key { + if ('key' in column && column.key !== undefined && column.key !== null) { + return column.key; + } + if (column.dataIndex) { + return (Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex) as Key; + } + + return defaultKey; } -export function treeMap(tree, mapper, childrenName = 'children') { - return tree.map((node, index) => { - const extra = {}; - if (node[childrenName]) { - extra[childrenName] = treeMap(node[childrenName], mapper, childrenName); - } - return { - ...mapper(node, index), - ...extra, - }; - }); +export function getColumnPos(index: number, pos?: string) { + return pos ? `${pos}-${index}` : `${index}`; } -export function flatFilter(tree, callback) { - return tree.reduce((acc, node) => { - if (callback(node)) { - acc.push(node); - } - if (node.children) { - const children = flatFilter(node.children, callback); - acc.push(...children); - } - return acc; - }, []); -} +export function renderColumnTitle( + title: ColumnTitle, + props: ColumnTitleProps, +) { + if (typeof title === 'function') { + return title(props); + } -// export function normalizeColumns (elements) { -// const columns = [] -// React.Children.forEach(elements, (element) => { -// if (!React.isValidElement(element)) { -// return -// } -// const column = { -// ...element.props, -// } -// if (element.key) { -// column.key = element.key -// } -// if (element.type && element.type.__ANT_TABLE_COLUMN_GROUP) { -// column.children = normalizeColumns(column.children) -// } -// columns.push(column) -// }) -// return columns -// } - -export function generateValueMaps(items, maps = {}) { - (items || []).forEach(({ value, children }) => { - maps[value.toString()] = value; - generateValueMaps(children, maps); - }); - return maps; + return title; } diff --git a/components/vc-table/Table.tsx b/components/vc-table/Table.tsx index 39f37b9ef..16811c345 100644 --- a/components/vc-table/Table.tsx +++ b/components/vc-table/Table.tsx @@ -6,8 +6,6 @@ import type { Key, TriggerEventHandler, GetComponentProps, - ExpandableConfig, - LegacyExpandableProps, PanelRender, TableLayout, RowClassName, @@ -15,6 +13,8 @@ import type { ColumnType, CustomizeScrollBody, TableSticky, + ExpandedRowRender, + RenderExpandIcon, } from './interface'; import Body from './Body'; import useColumns from './hooks/useColumns'; @@ -29,7 +29,7 @@ import { getCellFixedInfo } from './utils/fixUtil'; import StickyScrollBar from './stickyScrollBar'; import useSticky from './hooks/useSticky'; import FixedHolder from './FixedHolder'; -import type { CSSProperties } from 'vue'; +import type { CSSProperties, Ref } from 'vue'; import { computed, defineComponent, @@ -63,7 +63,7 @@ const EMPTY_SCROLL_TARGET = {}; export const INTERNAL_HOOKS = 'rc-table-internal-hook'; -export interface TableProps extends LegacyExpandableProps { +export interface TableProps { prefixCls?: string; data?: RecordType[]; columns?: ColumnsType; @@ -73,10 +73,6 @@ export interface TableProps extends LegacyExpandableProps< // Fixed Columns scroll?: { x?: number | true | string; y?: number | string }; - // Expandable - /** Config expand rows */ - expandable?: ExpandableConfig; - indentSize?: number; rowClassName?: string | RowClassName; // Additional Part @@ -94,17 +90,94 @@ export interface TableProps extends LegacyExpandableProps< direction?: 'ltr' | 'rtl'; + // Expandable expandFixed?: boolean; expandColumnWidth?: number; + expandedRowKeys?: Key[]; + defaultExpandedRowKeys?: Key[]; + expandedRowRender?: ExpandedRowRender; + expandRowByClick?: boolean; + expandIcon?: RenderExpandIcon; + onExpand?: (expanded: boolean, record: RecordType) => void; + onExpandedRowsChange?: (expandedKeys: Key[]) => void; + defaultExpandAllRows?: boolean; + indentSize?: number; expandIconColumnIndex?: number; + expandedRowClassName?: RowClassName; + 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) => ColumnsType; + + /** + * @private Internal usage, may remove by refactor. + * + * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!! + */ + internalRefs?: { + body: Ref; + }; sticky?: boolean | TableSticky; + + canExpandable?: boolean; } export default defineComponent({ name: 'Table', slots: ['title', 'footer', 'summary', 'emptyText'], emits: ['expand', 'expandedRowsChange'], + props: [ + 'prefixCls', + 'data', + 'columns', + 'rowKey', + 'tableLayout', + 'scroll', + 'rowClassName', + 'title', + 'footer', + 'id', + 'showHeader', + 'components', + 'customRow', + 'customHeaderRow', + 'direction', + 'expandFixed', + 'expandColumnWidth', + 'expandedRowKeys', + 'defaultExpandedRowKeys', + 'expandedRowRender', + 'expandRowByClick', + 'expandIcon', + 'onExpand', + 'onExpandedRowsChange', + 'defaultExpandAllRows', + 'indentSize', + 'expandIconColumnIndex', + 'expandedRowClassName', + 'childrenColumnName', + 'rowExpandable', + 'sticky', + 'transformColumns', + 'internalHooks', + 'internalRefs', + 'canExpandable', + ] as any, setup(props, { slots, emit }) { const mergedData = computed(() => props.data || EMPTY_DATA); const hasData = computed(() => !!mergedData.value.length); @@ -157,6 +230,7 @@ export default defineComponent({ * Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor */ if ( + props.canExpandable || mergedData.value.some( record => record && typeof record === 'object' && record[mergedChildrenColumnName.value], ) @@ -203,16 +277,19 @@ export default defineComponent({ const componentWidth = ref(0); - const [columns, flattenColumns] = useColumns({ - ...toRefs(props), + const [columns, flattenColumns] = useColumns( + { + ...toRefs(props), - // children, - expandable: computed(() => !!props.expandedRowRender), - expandedKeys: mergedExpandedKeys, - getRowKey, - onTriggerExpand, - expandIcon: mergedExpandIcon, - }); + // children, + expandable: computed(() => !!props.expandedRowRender), + expandedKeys: mergedExpandedKeys, + getRowKey, + onTriggerExpand, + expandIcon: mergedExpandIcon, + }, + computed(() => (props.internalHooks === INTERNAL_HOOKS ? props.transformColumns : null)), + ); const columnContext = computed(() => ({ columns: columns.value, @@ -377,6 +454,15 @@ export default defineComponent({ }); }); + watchEffect( + () => { + if (props.internalHooks === INTERNAL_HOOKS && props.internalRefs) { + props.internalRefs.body.value = scrollBodyRef.value; + } + }, + { flush: 'post' }, + ); + // Table layout const mergedTableLayout = computed(() => { if (props.tableLayout) { diff --git a/components/vc-table/hooks/useColumns.tsx b/components/vc-table/hooks/useColumns.tsx index ec411f8e7..7fbba233c 100644 --- a/components/vc-table/hooks/useColumns.tsx +++ b/components/vc-table/hooks/useColumns.tsx @@ -103,37 +103,39 @@ function revertForRtl(columns: ColumnsType): ColumnsType /** * Parse `columns` & `children` into `columns`. */ -function useColumns({ - prefixCls, - columns: baseColumns, - // children, - expandable, - expandedKeys, - getRowKey, - onTriggerExpand, - expandIcon, - rowExpandable, - expandIconColumnIndex, - direction, - expandRowByClick, - expandColumnWidth, - expandFixed, -}: { - prefixCls?: Ref; - columns?: Ref>; - expandable: Ref; - expandedKeys: Ref>; - getRowKey: Ref>; - onTriggerExpand: TriggerEventHandler; - expandIcon?: Ref>; - rowExpandable?: Ref<(record: RecordType) => boolean>; - expandIconColumnIndex?: Ref; - direction?: Ref<'ltr' | 'rtl'>; - expandRowByClick?: Ref; - expandColumnWidth?: Ref; - expandFixed?: Ref; -}): // transformColumns: (columns: ColumnsType) => ColumnsType, -[ComputedRef>, ComputedRef[]>] { +function useColumns( + { + prefixCls, + columns: baseColumns, + // children, + expandable, + expandedKeys, + getRowKey, + onTriggerExpand, + expandIcon, + rowExpandable, + expandIconColumnIndex, + direction, + expandRowByClick, + expandColumnWidth, + expandFixed, + }: { + prefixCls?: Ref; + columns?: Ref>; + expandable: Ref; + expandedKeys: Ref>; + getRowKey: Ref>; + onTriggerExpand: TriggerEventHandler; + expandIcon?: Ref>; + rowExpandable?: Ref<(record: RecordType) => boolean>; + expandIconColumnIndex?: Ref; + direction?: Ref<'ltr' | 'rtl'>; + expandRowByClick?: Ref; + expandColumnWidth?: Ref; + expandFixed?: Ref; + }, + transformColumns: Ref<(columns: ColumnsType) => ColumnsType>, +): [ComputedRef>, ComputedRef[]>] { // Add expand column const withExpandColumns = computed>(() => { if (expandable.value) { @@ -196,9 +198,9 @@ function useColumns({ const mergedColumns = computed(() => { let finalColumns = withExpandColumns.value; - // if (transformColumns) { - // finalColumns = transformColumns(finalColumns); - // } + if (transformColumns.value) { + finalColumns = transformColumns.value(finalColumns); + } // Always provides at least one column for table display if (!finalColumns.length) {