From 38569c28c7eb4eaa34f2cc096982daea901062d4 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Fri, 6 Aug 2021 23:27:54 +0800 Subject: [PATCH] perf: table use sticky for fixed column --- components/_util/hooks/useLayoutState.ts | 41 ++++++ components/table/style/index.less | 99 ++++++++++++-- components/vc-table/src/BaseTable.jsx | 19 +-- components/vc-table/src/ColGroup.jsx | 27 +++- components/vc-table/src/ColumnManager.jsx | 116 ---------------- components/vc-table/src/ExpandableTable.jsx | 6 +- components/vc-table/src/Table.jsx | 133 +++++++++++++------ components/vc-table/src/TableCell.jsx | 32 ++++- components/vc-table/src/TableHeader.jsx | 105 +++++++++------ components/vc-table/src/TableHeaderRow.jsx | 35 ++++- components/vc-table/src/TableRow.jsx | 1 + components/vc-table/src/fixUtil.ts | 73 ++++++++++ components/vc-table/src/useColumnManager.jsx | 97 ++++++++++++++ components/vc-table/src/useStickyOffsets.js | 43 ++++++ components/vc-table/src/utils.js | 16 +++ components/vc-util/Children/toArray.ts | 1 - package.json | 2 +- v2-doc | 2 +- 18 files changed, 613 insertions(+), 235 deletions(-) create mode 100644 components/_util/hooks/useLayoutState.ts delete mode 100644 components/vc-table/src/ColumnManager.jsx create mode 100644 components/vc-table/src/fixUtil.ts create mode 100644 components/vc-table/src/useColumnManager.jsx create mode 100644 components/vc-table/src/useStickyOffsets.js diff --git a/components/_util/hooks/useLayoutState.ts b/components/_util/hooks/useLayoutState.ts new file mode 100644 index 0000000000..753422ded4 --- /dev/null +++ b/components/_util/hooks/useLayoutState.ts @@ -0,0 +1,41 @@ +import type { Ref } from 'vue'; +import { onBeforeUnmount, ref } from 'vue'; +import wrapperRaf from '../raf'; + +export type Updater = (prev: State) => State; +/** + * Execute code before next frame but async + */ +export function useLayoutState( + defaultState: State, +): [Ref, (updater: Updater) => void] { + const stateRef = ref(defaultState); + let tempState = stateRef.value; + + let updateBatchRef = []; + const rafRef = ref(); + function setFrameState(updater: Updater) { + wrapperRaf.cancel(rafRef.value); + updateBatchRef.push(updater); + + rafRef.value = wrapperRaf(() => { + const prevBatch = updateBatchRef; + // const prevState = stateRef.value; + updateBatchRef = []; + + prevBatch.forEach(batchUpdater => { + tempState = batchUpdater(tempState); + }); + + // if (tempState !== stateRef.value) { + stateRef.value = tempState; + // } + }); + } + + onBeforeUnmount(() => { + wrapperRaf.cancel(rafRef.value); + }); + + return [stateRef as Ref, setFrameState]; +} diff --git a/components/table/style/index.less b/components/table/style/index.less index 962acd219f..ea2f31c6a7 100644 --- a/components/table/style/index.less +++ b/components/table/style/index.less @@ -615,15 +615,6 @@ overflow-x: hidden; table { min-width: 100%; - - // https://github.com/ant-design/ant-design/issues/14545 - // https://github.com/ant-design/ant-design/issues/19491 - .@{table-prefix-cls}-fixed-columns-in-body:not([colspan]) { - color: transparent; - & > * { - visibility: hidden; - } - } } } @@ -776,6 +767,96 @@ &-row[class*='@{table-prefix-cls}-row-level-0'] .@{table-prefix-cls}-selection-column > span { display: inline-block; } + + // ============================ Fixed ============================= + &-cell-fix-left, + &-cell-fix-right { + position: -webkit-sticky !important; + position: sticky !important; + z-index: @zindex-table-fixed; + background: @table-bg; + } + + &-cell-fix-left-first::after, + &-cell-fix-left-last::after { + position: absolute; + top: 0; + right: 0; + bottom: -1px; + width: 30px; + transform: translateX(100%); + transition: box-shadow 0.3s; + content: ''; + pointer-events: none; + } + + &-cell-fix-right-first::after, + &-cell-fix-right-last::after { + position: absolute; + top: 0; + bottom: -1px; + left: 0; + width: 30px; + transform: translateX(-100%); + transition: box-shadow 0.3s; + content: ''; + pointer-events: none; + } + + .@{table-prefix-cls}-container { + &::before, + &::after { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + width: 30px; + transition: box-shadow 0.3s; + content: ''; + pointer-events: none; + } + + &::before { + left: 0; + } + &::after { + right: 0; + } + } + + &-ping-left { + &:not(.@{table-prefix-cls}-has-fix-left) .@{table-prefix-cls}-container { + position: relative; + + &::before { + box-shadow: inset 10px 0 8px -8px darken(@shadow-color, 5%); + } + } + + .@{table-prefix-cls}-cell-fix-left-first::after, + .@{table-prefix-cls}-cell-fix-left-last::after { + box-shadow: inset 10px 0 8px -8px darken(@shadow-color, 5%); + } + + .@{table-prefix-cls}-cell-fix-left-last::before { + background-color: transparent !important; + } + } + + &-ping-right { + &:not(.@{table-prefix-cls}-has-fix-right) .@{table-prefix-cls}-container { + position: relative; + + &::after { + box-shadow: inset -10px 0 8px -8px darken(@shadow-color, 5%); + } + } + + .@{table-prefix-cls}-cell-fix-right-first::after, + .@{table-prefix-cls}-cell-fix-right-last::after { + box-shadow: inset -10px 0 8px -8px darken(@shadow-color, 5%); + } + } } .@{table-prefix-cls}-filter-dropdown, diff --git a/components/vc-table/src/BaseTable.jsx b/components/vc-table/src/BaseTable.jsx index a36ac67142..36bf0da97b 100644 --- a/components/vc-table/src/BaseTable.jsx +++ b/components/vc-table/src/BaseTable.jsx @@ -27,15 +27,10 @@ const BaseTable = { }, methods: { getColumns(cols) { - const { columns = [], fixed } = this.$props; - const { table } = this; - const { prefixCls } = table.$props; + const { columns = [] } = this.$props; return (cols || columns).map(column => ({ ...column, - className: - !!column.fixed && !fixed - ? classNames(`${prefixCls}-fixed-columns-in-body`, column.className, column.class) - : classNames(column.className, column.class), + className: classNames(column.className, column.class), })); }, handleRowHover(isHover, key) { @@ -44,7 +39,6 @@ const BaseTable = { renderRows(renderData, indent, ancestorKeys = []) { const { - columnManager, sComponents: components, prefixCls, childrenColumnName, @@ -57,6 +51,7 @@ const BaseTable = { onRowMouseLeave = noop, rowRef, } = { ...this.table.$attrs, ...this.table.$props, ...this.table.$data }; + const { columnManager } = this.store; const { getRowKey, fixed, expander, isAnyColumnsFixed } = this; const rows = []; @@ -68,17 +63,17 @@ const BaseTable = { typeof rowClassName === 'string' ? rowClassName : rowClassName(record, i, indent); const onHoverProps = {}; - if (columnManager.isAnyColumnsFixed()) { + if (columnManager.isAnyColumnsFixed) { onHoverProps.onHover = this.handleRowHover; } let leafColumns; if (fixed === 'left') { - leafColumns = columnManager.leftLeafColumns(); + leafColumns = columnManager.leftLeafColumns; } else if (fixed === 'right') { - leafColumns = columnManager.rightLeafColumns(); + leafColumns = columnManager.rightLeafColumns; } else { - leafColumns = this.getColumns(columnManager.leafColumns()); + leafColumns = this.getColumns(columnManager.leafColumns); } const rowPrefixCls = `${prefixCls}-row`; diff --git a/components/vc-table/src/ColGroup.jsx b/components/vc-table/src/ColGroup.jsx index 0a2c1e996f..a9d3cd13ab 100644 --- a/components/vc-table/src/ColGroup.jsx +++ b/components/vc-table/src/ColGroup.jsx @@ -1,6 +1,7 @@ import { inject } from 'vue'; import PropTypes from '../../_util/vue-types'; import { INTERNAL_COL_DEFINE } from './utils'; +import ResizeObserver from '../../vc-resize-observer'; export default { name: 'ColGroup', @@ -12,11 +13,12 @@ export default { setup() { return { table: inject('table', {}), + store: inject('table-store', () => ({})), }; }, render() { const { fixed, table } = this; - const { prefixCls, expandIconAsCell, columnManager } = table; + const { prefixCls, expandIconAsCell, onColumnResize } = table; let cols = []; @@ -25,19 +27,32 @@ export default { } let leafColumns; - + const { columnManager } = this.store; if (fixed === 'left') { - leafColumns = columnManager.leftLeafColumns(); + leafColumns = columnManager.leftLeafColumns; } else if (fixed === 'right') { - leafColumns = columnManager.rightLeafColumns(); + leafColumns = columnManager.rightLeafColumns; } else { - leafColumns = columnManager.leafColumns(); + leafColumns = columnManager.leafColumns; } cols = cols.concat( leafColumns.map(({ key, dataIndex, width, [INTERNAL_COL_DEFINE]: additionalProps }) => { const mergedKey = key !== undefined ? key : dataIndex; const w = typeof width === 'number' ? `${width}px` : width; - return ; + return ( + { + onColumnResize(mergedKey, offsetWidth); + }} + > + + + ); }), ); return {cols}; diff --git a/components/vc-table/src/ColumnManager.jsx b/components/vc-table/src/ColumnManager.jsx deleted file mode 100644 index 3f40ebf7b9..0000000000 --- a/components/vc-table/src/ColumnManager.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import { toRaw } from 'vue'; -export default class ColumnManager { - constructor(columns) { - this.columns = toRaw(columns); - this._cached = {}; - } - - isAnyColumnsFixed() { - return this._cache('isAnyColumnsFixed', () => this.columns.some(column => !!column.fixed)); - } - - isAnyColumnsLeftFixed() { - return this._cache('isAnyColumnsLeftFixed', () => - this.columns.some(column => column.fixed === 'left' || column.fixed === true), - ); - } - - isAnyColumnsRightFixed() { - return this._cache('isAnyColumnsRightFixed', () => - this.columns.some(column => column.fixed === 'right'), - ); - } - - leftColumns() { - return this._cache('leftColumns', () => - this.groupedColumns().filter(column => column.fixed === 'left' || column.fixed === true), - ); - } - - rightColumns() { - return this._cache('rightColumns', () => - this.groupedColumns().filter(column => column.fixed === 'right'), - ); - } - - leafColumns() { - return this._cache('leafColumns', () => this._leafColumns(this.columns)); - } - - leftLeafColumns() { - return this._cache('leftLeafColumns', () => this._leafColumns(this.leftColumns())); - } - - rightLeafColumns() { - return this._cache('rightLeafColumns', () => this._leafColumns(this.rightColumns())); - } - - // add appropriate rowspan and colspan to column - groupedColumns() { - return this._cache('groupedColumns', () => { - const _groupColumns = (columns, currentRow = 0, parentColumn = {}, rows = []) => { - // track how many rows we got - rows[currentRow] = rows[currentRow] || []; - const grouped = []; - const setRowSpan = column => { - const rowSpan = rows.length - currentRow; - if ( - column && - !column.children && // parent columns are supposed to be one row - rowSpan > 1 && - (!column.rowSpan || column.rowSpan < rowSpan) - ) { - column.rowSpan = rowSpan; - } - }; - columns.forEach((column, index) => { - const newColumn = { ...column }; - rows[currentRow].push(newColumn); - parentColumn.colSpan = parentColumn.colSpan || 0; - if (newColumn.children && newColumn.children.length > 0) { - newColumn.children = _groupColumns(newColumn.children, currentRow + 1, newColumn, rows); - parentColumn.colSpan += newColumn.colSpan; - } else { - parentColumn.colSpan += 1; - } - // update rowspan to all same row columns - for (let i = 0; i < rows[currentRow].length - 1; i += 1) { - setRowSpan(rows[currentRow][i]); - } - // last column, update rowspan immediately - if (index + 1 === columns.length) { - setRowSpan(newColumn); - } - grouped.push(newColumn); - }); - return grouped; - }; - return _groupColumns(this.columns); - }); - } - - reset(columns) { - this.columns = toRaw(columns); - this._cached = {}; - } - - _cache(name, fn) { - if (name in this._cached) { - return this._cached[name]; - } - this._cached[name] = fn(); - return this._cached[name]; - } - - _leafColumns(columns) { - const leafColumns = []; - columns.forEach(column => { - if (!column.children) { - leafColumns.push(column); - } else { - leafColumns.push(...this._leafColumns(column.children)); - } - }); - return leafColumns; - } -} diff --git a/components/vc-table/src/ExpandableTable.jsx b/components/vc-table/src/ExpandableTable.jsx index 58abe1591d..44b195f3e4 100644 --- a/components/vc-table/src/ExpandableTable.jsx +++ b/components/vc-table/src/ExpandableTable.jsx @@ -147,11 +147,11 @@ const ExpandableTable = { }; let colCount; if (fixed === 'left') { - colCount = this.columnManager.leftLeafColumns().length; + colCount = this.columnManager.leftLeafColumns.value.length; } else if (fixed === 'right') { - colCount = this.columnManager.rightLeafColumns().length; + colCount = this.columnManager.rightLeafColumns.value.length; } else { - colCount = this.columnManager.leafColumns().length; + colCount = this.columnManager.leafColumns.value.length; } const columns = [ { diff --git a/components/vc-table/src/Table.jsx b/components/vc-table/src/Table.jsx index 004f48b4a9..4d1cdd5911 100644 --- a/components/vc-table/src/Table.jsx +++ b/components/vc-table/src/Table.jsx @@ -1,19 +1,33 @@ /* eslint-disable camelcase */ -import { provide, markRaw, defineComponent, nextTick, reactive } from 'vue'; +import { + provide, + markRaw, + defineComponent, + nextTick, + reactive, + computed, + ref, + onUpdated, + onMounted, +} from 'vue'; import shallowequal from '../../_util/shallowequal'; import merge from 'lodash-es/merge'; import classes from '../../_util/component-classes'; import classNames from '../../_util/classNames'; import PropTypes from '../../_util/vue-types'; -import { debounce, getDataAndAriaProps } from './utils'; +import { debounce, getColumnsKey, getDataAndAriaProps, validateValue } from './utils'; import warning from '../../_util/warning'; import addEventListener from '../../vc-util/Dom/addEventListener'; -import ColumnManager from './ColumnManager'; import HeadTable from './HeadTable'; import BodyTable from './BodyTable'; import ExpandableTable from './ExpandableTable'; import { initDefaultProps, getOptionProps } from '../../_util/props-util'; import BaseMixin from '../../_util/BaseMixin'; +import { useLayoutState } from '../../_util/hooks/useLayoutState'; +import useColumnManager from './useColumnManager'; +import useStickyOffsets from './useStickyOffsets'; +import { getCellFixedInfo } from './fixUtil'; +import ResizeObserver from '../../vc-resize-observer'; export default defineComponent({ name: 'Table', @@ -84,23 +98,76 @@ export default defineComponent({ customHeaderRow: () => {}, }, ), - setup() { + setup(props) { + const columnManager = useColumnManager(props.columns); + const colsKeys = computed(() => getColumnsKey(columnManager.leafColumns.value)); + const [colsWidths, updateColsWidths] = useLayoutState(new Map()); + const pureColWidths = computed(() => + colsKeys.value.map(columnKey => colsWidths.value.get(columnKey)), + ); + const stickyOffsets = useStickyOffsets(pureColWidths, columnManager.leafColumns); + const onColumnResize = (columnKey, width) => { + updateColsWidths(widths => { + if (widths.get(columnKey) !== width) { + const newWidths = new Map(widths); + newWidths.set(columnKey, width); + return newWidths; + } + return widths; + }); + }; + const fixedInfoList = computed(() => + columnManager.leafColumns.value.map((_, colIndex) => + getCellFixedInfo(colIndex, colIndex, columnManager.leafColumns.value, stickyOffsets.value), + ), + ); const store = reactive({ currentHoverKey: null, fixedColumnsHeadRowsHeight: [], fixedColumnsBodyRowsHeight: {}, expandedRowsHeight: {}, expandedRowKeys: [], + columnManager, + fixedInfoList, + stickyOffsets, }); provide('table-store', store); + const bodyRef = ref(); + const pingedLeft = ref(false); + const pingedRight = ref(false); + const horizonScroll = computed(() => props.scroll && validateValue(props.scroll.x)); + const onScroll = currentTarget => { + const { scrollWidth, clientWidth, scrollLeft } = currentTarget; + pingedLeft.value = scrollLeft > 0; + pingedRight.value = scrollLeft < scrollWidth - clientWidth; + }; + onUpdated(() => { + nextTick(() => { + horizonScroll.value && onScroll(bodyRef.value.$el); + }); + }); + onMounted(() => { + nextTick(() => { + horizonScroll.value && onScroll(bodyRef.value.$el); + }); + }); + const onFullTableResize = () => { + horizonScroll.value && onScroll(bodyRef.value.$el); + }; return { + bodyRef, store, + onColumnResize, + columnManager, + onScroll, + pingedLeft, + pingedRight, + onFullTableResize, }; }, data() { this.preData = [...this.data]; return { - columnManager: markRaw(new ColumnManager(this.columns)), sComponents: markRaw( merge( { @@ -145,11 +212,6 @@ export default defineComponent({ this.components, ); }, - columns(val) { - if (val) { - this.columnManager.reset(val); - } - }, dataLen(val, preVal) { if ((val === 0 || preVal === 0) && this.hasScrollX()) { nextTick(() => { @@ -160,21 +222,6 @@ export default defineComponent({ }, created() { provide('table', this); - // ['rowClick', 'rowDoubleclick', 'rowContextmenu', 'rowMouseenter', 'rowMouseleave'].forEach( - // name => { - // warning( - // getListeners(this)[name] === undefined, - // `${name} is deprecated, please use customRow instead.`, - // ); - // }, - // ); - - // warning( - // this.getBodyWrapper === undefined, - // 'getBodyWrapper is deprecated, please use custom components instead.', - // ); - - // this.columnManager = new ColumnManager(this.columns, this.$slots.default) this.setScrollPosition('left'); @@ -183,7 +230,7 @@ export default defineComponent({ mounted() { this.$nextTick(() => { - if (this.columnManager.isAnyColumnsFixed()) { + if (this.columnManager.isAnyColumnsFixed.value) { this.handleWindowResize(); this.resizeEvent = addEventListener(window, 'resize', this.debouncedWindowResize); } @@ -199,7 +246,7 @@ export default defineComponent({ updated() { this.$nextTick(() => { - if (this.columnManager.isAnyColumnsFixed()) { + if (this.columnManager.isAnyColumnsFixed.value) { this.handleWindowResize(); if (!this.resizeEvent) { this.resizeEvent = addEventListener(window, 'resize', this.debouncedWindowResize); @@ -383,8 +430,8 @@ export default defineComponent({ // Remember last scrollTop for scroll direction detecting. this.lastScrollTop = target.scrollTop; }, - handleBodyScroll(e) { + this.onScroll(e.target); this.handleBodyScrollLeft(e); this.handleBodyScrollTop(e); }, @@ -432,28 +479,34 @@ export default defineComponent({ }, renderMainTable() { const { scroll, prefixCls } = this; - const isAnyColumnsFixed = this.columnManager.isAnyColumnsFixed(); + const isAnyColumnsFixed = this.columnManager.isAnyColumnsFixed.value; const scrollable = isAnyColumnsFixed || scroll.x || scroll.y; const table = [ this.renderTable({ - columns: this.columnManager.groupedColumns(), + columns: this.columnManager.groupedColumns.value, isAnyColumnsFixed, }), this.renderEmptyText(), this.renderFooter(), ]; - return scrollable ?
{table}
: table; + return scrollable ? ( + +
{table}
+
+ ) : ( + table + ); }, renderLeftFixedTable() { const { prefixCls } = this; return ( -
+
{this.renderTable({ - columns: this.columnManager.leftColumns(), + columns: this.columnManager.leftColumns.value, fixed: 'left', })}
@@ -465,7 +518,7 @@ export default defineComponent({ return (
{this.renderTable({ - columns: this.columnManager.rightColumns(), + columns: this.columnManager.rightColumns.value, fixed: 'right', })}
@@ -499,6 +552,7 @@ export default defineComponent({ handleBodyScroll={this.handleBodyScroll} expander={this.expander} isAnyColumnsFixed={isAnyColumnsFixed} + ref="bodyRef" /> ); @@ -548,10 +602,9 @@ export default defineComponent({ this.scrollPosition === 'both', [`${prefixCls}-scroll-position-${this.scrollPosition}`]: this.scrollPosition !== 'both', [`${prefixCls}-layout-fixed`]: this.isTableLayoutFixed(), + [`${prefixCls}-ping-left`]: this.pingedLeft, + [`${prefixCls}-ping-right`]: this.pingedRight, }); - - const hasLeftFixed = columnManager.isAnyColumnsLeftFixed(); - const hasRightFixed = columnManager.isAnyColumnsRightFixed(); const dataAndAriaProps = getDataAndAriaProps(props); const expandableTableProps = { ...props, @@ -573,11 +626,7 @@ export default defineComponent({ {...dataAndAriaProps} > {this.renderTitle()} -
- {this.renderMainTable()} - {hasLeftFixed && this.renderLeftFixedTable()} - {hasRightFixed && this.renderRightFixedTable()} -
+
{this.renderMainTable()}
); }, diff --git a/components/vc-table/src/TableCell.jsx b/components/vc-table/src/TableCell.jsx index d7893ce429..7312883c62 100644 --- a/components/vc-table/src/TableCell.jsx +++ b/components/vc-table/src/TableCell.jsx @@ -22,10 +22,12 @@ export default { column: PropTypes.object, expandIcon: PropTypes.any, component: PropTypes.any, + colIndex: PropTypes.number, }, setup() { return { table: inject('table', {}), + store: inject('table-store', {}), }; }, methods: { @@ -51,8 +53,24 @@ export default { column, component: BodyCell, } = this; + const fixedInfoList = this.store.fixedInfoList || []; + const fixedInfo = fixedInfoList[this.colIndex]; + const { fixLeft, fixRight, firstFixLeft, lastFixLeft, firstFixRight, lastFixRight } = fixedInfo; + // ====================== Fixed ======================= + const fixedStyle = {}; + const isFixLeft = typeof fixLeft === 'number'; + const isFixRight = typeof fixRight === 'number'; + + if (isFixLeft) { + fixedStyle.position = 'sticky'; + fixedStyle.left = `${fixLeft}px`; + } + if (isFixRight) { + fixedStyle.position = 'sticky'; + fixedStyle.right = `${fixRight}px`; + } const { dataIndex, customRender, className = '' } = column; - const { transformCellText } = this.table; + const { transformCellText, prefixCls: rootPrefixCls } = this.table; // We should return undefined if no dataIndex is specified, but in order to // be compatible with object-path's behavior, we return the record object instead. let text; @@ -110,6 +128,12 @@ export default { // 如果有宽度,增加断行处理 // https://github.com/ant-design/ant-design/issues/13825#issuecomment-449889241 [`${prefixCls}-cell-break-word`]: !!column.width, + [`${rootPrefixCls}-cell-fix-left`]: isFixLeft, + [`${rootPrefixCls}-cell-fix-left-first`]: firstFixLeft, + [`${rootPrefixCls}-cell-fix-left-last`]: lastFixLeft, + [`${rootPrefixCls}-cell-fix-right`]: isFixRight, + [`${rootPrefixCls}-cell-fix-right-first`]: firstFixRight, + [`${rootPrefixCls}-cell-fix-right-last`]: lastFixRight, }); if (column.ellipsis) { @@ -124,7 +148,11 @@ export default { } return ( - + {indentText} {expandIcon} {toRaw(text)} diff --git a/components/vc-table/src/TableHeader.jsx b/components/vc-table/src/TableHeader.jsx index cb83474d80..aee123792b 100644 --- a/components/vc-table/src/TableHeader.jsx +++ b/components/vc-table/src/TableHeader.jsx @@ -1,44 +1,70 @@ -import { inject } from 'vue'; +import { computed, inject } from 'vue'; import PropTypes from '../../_util/vue-types'; import TableHeaderRow from './TableHeaderRow'; -function getHeaderRows({ columns = [], currentRow = 0, rows = [], isLast = true }) { - rows = rows || []; - rows[currentRow] = rows[currentRow] || []; +function parseHeaderRows(rootColumns) { + const rows = []; - columns.forEach((column, i) => { - if (column.rowSpan && rows.length < column.rowSpan) { - while (rows.length < column.rowSpan) { - rows.push([]); + function fillRowCells(columns, colIndex, rowIndex = 0) { + // Init rows + rows[rowIndex] = rows[rowIndex] || []; + + let currentColIndex = colIndex; + const colSpans = columns.filter(Boolean).map(column => { + const cell = { + key: column.key, + className: column.className || column.class || '', + children: column.title, + column, + colStart: currentColIndex, + }; + + let colSpan = 1; + + const subColumns = column.children; + if (subColumns && subColumns.length > 0) { + colSpan = fillRowCells(subColumns, currentColIndex, rowIndex + 1).reduce( + (total, count) => total + count, + 0, + ); + cell.hasSubColumns = true; } - } - const cellIsLast = isLast && i === columns.length - 1; - const cell = { - key: column.key, - className: column.className || column.class || '', - children: column.title, - isLast: cellIsLast, - column, - }; - if (column.children) { - getHeaderRows({ - columns: column.children, - currentRow: currentRow + 1, - rows, - isLast: cellIsLast, - }); - } - if ('colSpan' in column) { - cell.colSpan = column.colSpan; - } - if ('rowSpan' in column) { - cell.rowSpan = column.rowSpan; - } - if (cell.colSpan !== 0) { - rows[currentRow].push(cell); - } - }); - return rows.filter(row => row.length > 0); + + if ('colSpan' in column) { + ({ colSpan } = column); + } + + if ('rowSpan' in column) { + cell.rowSpan = column.rowSpan; + } + + cell.colSpan = colSpan; + cell.colEnd = cell.colStart + colSpan - 1; + rows[rowIndex].push(cell); + + currentColIndex += colSpan; + + return colSpan; + }); + + return colSpans; + } + + // Generate `rows` cell data + fillRowCells(rootColumns, 0); + + // Handle `rowSpan` + const rowCount = rows.length; + for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { + rows[rowIndex].forEach(cell => { + if (!('rowSpan' in cell) && !cell.hasSubColumns) { + // eslint-disable-next-line no-param-reassign + cell.rowSpan = rowCount - rowIndex; + } + }); + } + + return rows; } export default { @@ -49,22 +75,21 @@ export default { columns: PropTypes.array.isRequired, expander: PropTypes.object.isRequired, }, - setup() { + setup(props) { return { table: inject('table', {}), + rows: computed(() => parseHeaderRows(props.columns)), }; }, render() { const { sComponents: components, prefixCls, showHeader, customHeaderRow } = this.table; - const { expander, columns, fixed } = this; + const { expander, columns, fixed, rows } = this; if (!showHeader) { return null; } - const rows = getHeaderRows({ columns }); - expander.renderExpandIndentCell(rows, fixed); const HeaderWrapper = components.header.wrapper; diff --git a/components/vc-table/src/TableHeaderRow.jsx b/components/vc-table/src/TableHeaderRow.jsx index 833177867e..d41de3e352 100644 --- a/components/vc-table/src/TableHeaderRow.jsx +++ b/components/vc-table/src/TableHeaderRow.jsx @@ -1,6 +1,7 @@ import classNames from '../../_util/classNames'; import PropTypes from '../../_util/vue-types'; import { computed, inject } from 'vue'; +import { getCellFixedInfo } from './fixUtil'; const TableHeaderRow = { name: 'TableHeaderRow', @@ -35,6 +36,7 @@ const TableHeaderRow = { } return null; }), + store, }; }, render() { @@ -50,21 +52,44 @@ const TableHeaderRow = { if (style.height === null) { delete style.height; } + const { stickyOffsets, columnManager } = this.store; return ( {row.map((cell, i) => { const { column, isLast, children, className, ...cellProps } = cell; + const fixedInfo = getCellFixedInfo( + cell.colStart, + cell.colEnd, + columnManager.leafColumns, + stickyOffsets, + ); const customProps = column.customHeaderCell ? column.customHeaderCell(column) : {}; const headerCellProps = { ...cellProps, ...customProps, key: column.key || column.dataIndex || i, }; - + if (headerCellProps.colSpan === 0) { + return null; + } if (column.align) { headerCellProps.style = { ...customProps.style, textAlign: column.align }; } + // ====================== Fixed ======================= + const { fixLeft, fixRight, firstFixLeft, lastFixLeft, firstFixRight, lastFixRight } = + fixedInfo; + const fixedStyle = {}; + const isFixLeft = typeof fixLeft === 'number'; + const isFixRight = typeof fixRight === 'number'; + if (isFixLeft) { + fixedStyle.position = 'sticky'; + fixedStyle.left = `${fixLeft}px`; + } + if (isFixRight) { + fixedStyle.position = 'sticky'; + fixedStyle.right = `${fixRight}px`; + } headerCellProps.class = classNames( customProps.class, customProps.className, @@ -75,9 +100,15 @@ const TableHeaderRow = { [`${prefixCls}-row-cell-ellipsis`]: !!column.ellipsis, [`${prefixCls}-row-cell-break-word`]: !!column.width, [`${prefixCls}-row-cell-last`]: isLast, + [`${prefixCls}-cell-fix-left`]: isFixLeft, + [`${prefixCls}-cell-fix-left-first`]: firstFixLeft, + [`${prefixCls}-cell-fix-left-last`]: lastFixLeft, + [`${prefixCls}-cell-fix-right`]: isFixRight, + [`${prefixCls}-cell-fix-right-first`]: firstFixRight, + [`${prefixCls}-cell-fix-right-last`]: lastFixRight, }, ); - + headerCellProps.style = { ...(headerCellProps.style || {}), ...fixedStyle }; if (typeof HeaderCell === 'function') { return HeaderCell(headerCellProps, children); } diff --git a/components/vc-table/src/TableRow.jsx b/components/vc-table/src/TableRow.jsx index 9b6544d247..bb8c38c707 100644 --- a/components/vc-table/src/TableRow.jsx +++ b/components/vc-table/src/TableRow.jsx @@ -250,6 +250,7 @@ const TableRow = { indentSize={indentSize} indent={indent} index={index} + colIndex={i} column={column} key={column.key || column.dataIndex} expandIcon={hasExpandIcon(i) && renderExpandIcon()} diff --git a/components/vc-table/src/fixUtil.ts b/components/vc-table/src/fixUtil.ts new file mode 100644 index 0000000000..4b4b40a8f6 --- /dev/null +++ b/components/vc-table/src/fixUtil.ts @@ -0,0 +1,73 @@ +export interface StickyOffsets { + left: readonly number[]; + right: readonly number[]; + isSticky?: boolean; +} +export type FixedType = 'left' | 'right' | boolean; +export interface FixedInfo { + fixLeft: number | false; + fixRight: number | false; + lastFixLeft: boolean; + firstFixRight: boolean; + + // For Rtl Direction + lastFixRight: boolean; + firstFixLeft: boolean; + + isSticky: boolean; +} + +export function getCellFixedInfo( + colStart: number, + colEnd: number, + columns: readonly { fixed?: FixedType }[], + stickyOffsets: StickyOffsets, + direction: 'ltr' | 'rtl', +): FixedInfo { + const startColumn = columns[colStart] || {}; + const endColumn = columns[colEnd] || {}; + + let fixLeft: number; + let fixRight: number; + + if (startColumn.fixed === 'left') { + fixLeft = stickyOffsets.left[colStart]; + } else if (endColumn.fixed === 'right') { + fixRight = stickyOffsets.right[colEnd]; + } + + let lastFixLeft = false; + let firstFixRight = false; + + let lastFixRight = false; + let firstFixLeft = false; + + const nextColumn = columns[colEnd + 1]; + const prevColumn = columns[colStart - 1]; + + if (direction === 'rtl') { + if (fixLeft !== undefined) { + const prevFixLeft = prevColumn && prevColumn.fixed === 'left'; + firstFixLeft = !prevFixLeft; + } else if (fixRight !== undefined) { + const nextFixRight = nextColumn && nextColumn.fixed === 'right'; + lastFixRight = !nextFixRight; + } + } else if (fixLeft !== undefined) { + const nextFixLeft = nextColumn && nextColumn.fixed === 'left'; + lastFixLeft = !nextFixLeft; + } else if (fixRight !== undefined) { + const prevFixRight = prevColumn && prevColumn.fixed === 'right'; + firstFixRight = !prevFixRight; + } + + return { + fixLeft, + fixRight, + lastFixLeft, + firstFixRight, + lastFixRight, + firstFixLeft, + isSticky: stickyOffsets.isSticky, + }; +} diff --git a/components/vc-table/src/useColumnManager.jsx b/components/vc-table/src/useColumnManager.jsx new file mode 100644 index 0000000000..d8cfb22f56 --- /dev/null +++ b/components/vc-table/src/useColumnManager.jsx @@ -0,0 +1,97 @@ +import { computed } from 'vue'; +export default function useColumnManager(columns) { + const _leafColumns = (cls, fixed = false) => { + const leafColumns = []; + cls.forEach(column => { + column.fixed = fixed || column.fixed; + if (!column.children) { + leafColumns.push(column); + } else { + leafColumns.push(..._leafColumns(column.children, column.fixed)); + } + }); + return leafColumns; + }; + + // add appropriate rowspan and colspan to column + const groupedColumns = computed(() => { + const _groupColumns = (cls, currentRow = 0, parentColumn = {}, rows = [], fixed = false) => { + // track how many rows we got + rows[currentRow] = rows[currentRow] || []; + const grouped = []; + const setRowSpan = column => { + const rowSpan = rows.length - currentRow; + if ( + column && + !column.children && // parent columns are supposed to be one row + rowSpan > 1 && + (!column.rowSpan || column.rowSpan < rowSpan) + ) { + column.rowSpan = rowSpan; + } + }; + cls.forEach((column, index) => { + const newColumn = { ...column }; + newColumn.fixed = fixed || column.fixed; + rows[currentRow].push(newColumn); + parentColumn.colSpan = parentColumn.colSpan || 0; + if (newColumn.children && newColumn.children.length > 0) { + newColumn.children = _groupColumns( + newColumn.children, + currentRow + 1, + newColumn, + rows, + newColumn.fixed, + ); + parentColumn.colSpan += newColumn.colSpan; + } else { + parentColumn.colSpan += 1; + } + // update rowspan to all same row columns + for (let i = 0; i < rows[currentRow].length - 1; i += 1) { + setRowSpan(rows[currentRow][i]); + } + // last column, update rowspan immediately + if (index + 1 === cls.length) { + setRowSpan(newColumn); + } + grouped.push(newColumn); + }); + return grouped; + }; + return _groupColumns(columns); + }); + + const isAnyColumnsFixed = computed(() => columns.some(column => !!column.fixed)); + + const isAnyColumnsLeftFixed = computed(() => + columns.some(column => column.fixed === 'left' || column.fixed === true), + ); + + const isAnyColumnsRightFixed = computed(() => columns.some(column => column.fixed === 'right')); + + const leftColumns = computed(() => + groupedColumns.value.filter(column => column.fixed === 'left' || column.fixed === true), + ); + + const rightColumns = computed(() => { + return groupedColumns.value.filter(column => column.fixed === 'right'); + }); + + const leafColumns = computed(() => _leafColumns(columns)); + + const leftLeafColumns = computed(() => _leafColumns(leftColumns.value)); + + const rightLeafColumns = computed(() => _leafColumns(rightColumns.value)); + return { + groupedColumns, + isAnyColumnsFixed, + isAnyColumnsLeftFixed, + isAnyColumnsRightFixed, + leftColumns, + rightColumns, + leafColumns, + leftLeafColumns, + rightLeafColumns, + }; +} diff --git a/components/vc-table/src/useStickyOffsets.js b/components/vc-table/src/useStickyOffsets.js new file mode 100644 index 0000000000..0005935bc7 --- /dev/null +++ b/components/vc-table/src/useStickyOffsets.js @@ -0,0 +1,43 @@ +import { ref, watch } from 'vue'; + +/** + * Get sticky column offset width + */ +function useStickyOffsets(colWidths, columns) { + const stickyOffsets = ref({ + left: [], + right: [], + }); + const columnCount = ref(); + watch( + columns, + () => { + columnCount.value = columns.value.length; + }, + { immediate: true }, + ); + watch([colWidths, columnCount], () => { + const leftOffsets = []; + const rightOffsets = []; + let left = 0; + let right = 0; + + for (let start = 0; start < columnCount.value; start += 1) { + // Left offset + leftOffsets[start] = left; + left += colWidths.value[start] || 0; + + // Right offset + const end = columnCount.value - start - 1; + rightOffsets[end] = right; + right += colWidths.value[end] || 0; + } + stickyOffsets.value = { + left: leftOffsets, + right: rightOffsets, + }; + }); + return stickyOffsets; +} + +export default useStickyOffsets; diff --git a/components/vc-table/src/utils.js b/components/vc-table/src/utils.js index 3fb659c275..eacbf6fa4d 100644 --- a/components/vc-table/src/utils.js +++ b/components/vc-table/src/utils.js @@ -98,3 +98,19 @@ export function getDataAndAriaProps(props) { return memo; }, {}); } + +export function getColumnsKey(columns) { + const columnKeys = []; + + columns.forEach(column => { + const { key, dataIndex } = column || {}; + + columnKeys.push(key !== undefined ? key : dataIndex); + }); + + return columnKeys; +} + +export function validateValue(val) { + return val !== null && val !== undefined; +} diff --git a/components/vc-util/Children/toArray.ts b/components/vc-util/Children/toArray.ts index 0257fcd490..2da784e42e 100644 --- a/components/vc-util/Children/toArray.ts +++ b/components/vc-util/Children/toArray.ts @@ -12,7 +12,6 @@ export default function toArray(children: any[], option: Option = {}): any[] { if ((child === undefined || child === null) && !option.keepEmpty) { return; } - debugger; if (Array.isArray(child)) { ret = ret.concat(toArray(child)); } else if (isFragment(child) && child.props) { diff --git a/package.json b/package.json index 7db46b41ef..a84e79b1cc 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "vue-clipboard2": "0.3.1", "vue-draggable-resizable": "^2.1.0", "vue-eslint-parser": "^7.0.0", - "vue-i18n": "^9.0.0-alpha.11", + "vue-i18n": "^9.1.7", "vue-infinite-scroll": "^2.0.2", "vue-jest": "^5.0.0-alpha.3", "vue-loader": "^16.1.1", diff --git a/v2-doc b/v2-doc index e67d637297..1882ee4e30 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit e67d63729715291f481029fe9497f0109f328504 +Subproject commit 1882ee4e30b707551a08561f090e72a3f261f6cf