perf: table use sticky for fixed column

pull/4499/head
tangjinzhou 2021-08-06 23:27:54 +08:00
parent 4fed700e96
commit 38569c28c7
18 changed files with 613 additions and 235 deletions

View File

@ -0,0 +1,41 @@
import type { Ref } from 'vue';
import { onBeforeUnmount, ref } from 'vue';
import wrapperRaf from '../raf';
export type Updater<State> = (prev: State) => State;
/**
* Execute code before next frame but async
*/
export function useLayoutState<State>(
defaultState: State,
): [Ref<State>, (updater: Updater<State>) => void] {
const stateRef = ref(defaultState);
let tempState = stateRef.value;
let updateBatchRef = [];
const rafRef = ref();
function setFrameState(updater: Updater<State>) {
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<State>, setFrameState];
}

View File

@ -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,

View File

@ -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`;

View File

@ -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 <col key={mergedKey} style={{ width: w, minWidth: w }} {...additionalProps} />;
return (
<ResizeObserver
onResize={({ offsetWidth }) => {
onColumnResize(mergedKey, offsetWidth);
}}
>
<col
data-key={mergedKey}
key={mergedKey}
style={{ width: w, minWidth: w }}
{...additionalProps}
/>
</ResizeObserver>
);
}),
);
return <colgroup>{cols}</colgroup>;

View File

@ -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;
}
}

View File

@ -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 = [
{

View File

@ -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 ? <div class={`${prefixCls}-scroll`}>{table}</div> : table;
return scrollable ? (
<ResizeObserver onResize={this.onFullTableResize}>
<div class={`${prefixCls}-scroll`}>{table}</div>
</ResizeObserver>
) : (
table
);
},
renderLeftFixedTable() {
const { prefixCls } = this;
return (
<div class={`${prefixCls}-fixed-left`}>
<div key="left" class={`${prefixCls}-fixed-left`}>
{this.renderTable({
columns: this.columnManager.leftColumns(),
columns: this.columnManager.leftColumns.value,
fixed: 'left',
})}
</div>
@ -465,7 +518,7 @@ export default defineComponent({
return (
<div class={`${prefixCls}-fixed-right`}>
{this.renderTable({
columns: this.columnManager.rightColumns(),
columns: this.columnManager.rightColumns.value,
fixed: 'right',
})}
</div>
@ -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()}
<div class={`${prefixCls}-content`}>
{this.renderMainTable()}
{hasLeftFixed && this.renderLeftFixedTable()}
{hasRightFixed && this.renderRightFixedTable()}
</div>
<div class={`${prefixCls}-content`}>{this.renderMainTable()}</div>
</div>
);
},

View File

@ -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 (
<BodyCell class={cellClassName} {...tdProps}>
<BodyCell
class={cellClassName}
{...tdProps}
{...{ style: { ...(tdProps.style || {}), ...fixedStyle } }}
>
{indentText}
{expandIcon}
{toRaw(text)}

View File

@ -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;

View File

@ -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 (
<HeaderRow {...rowProps} style={style}>
{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);
}

View File

@ -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()}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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) {

View File

@ -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",

2
v2-doc

@ -1 +1 @@
Subproject commit e67d63729715291f481029fe9497f0109f328504
Subproject commit 1882ee4e30b707551a08561f090e72a3f261f6cf