ant-design-vue/components/vc-table/src/Table.jsx

600 lines
17 KiB
Vue

/* eslint-disable camelcase */
import shallowequal from 'shallowequal';
import merge from 'lodash/merge';
import classes from 'component-classes';
import classNames from 'classnames';
import PropTypes from '../../_util/vue-types';
import { debounce } from './utils';
import warning from '../../_util/warning';
import addEventListener from '../../vc-util/Dom/addEventListener';
import { Provider, create } from '../../_util/store';
import ColumnManager from './ColumnManager';
import HeadTable from './HeadTable';
import BodyTable from './BodyTable';
import ExpandableTable from './ExpandableTable';
import { initDefaultProps, getOptionProps, getListeners } from '../../_util/props-util';
import BaseMixin from '../../_util/BaseMixin';
export default {
name: 'Table',
mixins: [BaseMixin],
props: initDefaultProps(
{
data: PropTypes.array,
useFixedHeader: PropTypes.bool,
columns: PropTypes.array,
prefixCls: PropTypes.string,
bodyStyle: PropTypes.object,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
customRow: PropTypes.func,
customHeaderRow: PropTypes.func,
// onRowClick: PropTypes.func,
// onRowDoubleClick: PropTypes.func,
// onRowContextMenu: PropTypes.func,
// onRowMouseEnter: PropTypes.func,
// onRowMouseLeave: PropTypes.func,
showHeader: PropTypes.bool,
title: PropTypes.func,
id: PropTypes.string,
footer: PropTypes.func,
emptyText: PropTypes.any,
scroll: PropTypes.object,
rowRef: PropTypes.func,
getBodyWrapper: PropTypes.func,
components: PropTypes.shape({
table: PropTypes.any,
header: PropTypes.shape({
wrapper: PropTypes.any,
row: PropTypes.any,
cell: PropTypes.any,
}),
body: PropTypes.shape({
wrapper: PropTypes.any,
row: PropTypes.any,
cell: PropTypes.any,
}),
}),
expandIconAsCell: PropTypes.bool,
expandedRowKeys: PropTypes.array,
expandedRowClassName: PropTypes.func,
defaultExpandAllRows: PropTypes.bool,
defaultExpandedRowKeys: PropTypes.array,
expandIconColumnIndex: PropTypes.number,
expandedRowRender: PropTypes.func,
childrenColumnName: PropTypes.string,
indentSize: PropTypes.number,
expandRowByClick: PropTypes.bool,
expandIcon: PropTypes.func,
tableLayout: PropTypes.string,
},
{
data: [],
useFixedHeader: false,
rowKey: 'key',
rowClassName: () => '',
prefixCls: 'rc-table',
bodyStyle: {},
showHeader: true,
scroll: {},
rowRef: () => null,
emptyText: () => 'No Data',
customHeaderRow: () => {},
},
),
data() {
this.preData = [...this.data];
return {
columnManager: new ColumnManager(this.columns),
sComponents: merge(
{
table: 'table',
header: {
wrapper: 'thead',
row: 'tr',
cell: 'th',
},
body: {
wrapper: 'tbody',
row: 'tr',
cell: 'td',
},
},
this.components,
),
};
},
watch: {
components() {
this._components = merge(
{
table: 'table',
header: {
wrapper: 'thead',
row: 'tr',
cell: 'th',
},
body: {
wrapper: 'tbody',
row: 'tr',
cell: 'td',
},
},
this.components,
);
},
columns(val) {
if (val) {
this.columnManager.reset(val);
}
},
data(val) {
if (val.length === 0 && this.hasScrollX()) {
this.$nextTick(() => {
this.resetScrollX();
});
}
},
},
// static childContextTypes = {
// table: PropTypes.any,
// components: PropTypes.any,
// },
created() {
['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.store = create({
currentHoverKey: null,
fixedColumnsHeadRowsHeight: [],
fixedColumnsBodyRowsHeight: {},
});
this.setScrollPosition('left');
this.debouncedWindowResize = debounce(this.handleWindowResize, 150);
},
provide() {
return {
table: this,
};
},
mounted() {
this.$nextTick(() => {
if (this.columnManager.isAnyColumnsFixed()) {
this.handleWindowResize();
this.resizeEvent = addEventListener(window, 'resize', this.debouncedWindowResize);
}
// https://github.com/ant-design/ant-design/issues/11635
if (this.ref_headTable) {
this.ref_headTable.scrollLeft = 0;
}
if (this.ref_bodyTable) {
this.ref_bodyTable.scrollLeft = 0;
}
});
},
updated() {
this.$nextTick(() => {
if (this.columnManager.isAnyColumnsFixed()) {
this.handleWindowResize();
if (!this.resizeEvent) {
this.resizeEvent = addEventListener(window, 'resize', this.debouncedWindowResize);
}
}
});
},
beforeDestroy() {
if (this.resizeEvent) {
this.resizeEvent.remove();
}
if (this.debouncedWindowResize) {
this.debouncedWindowResize.cancel();
}
},
methods: {
getRowKey(record, index) {
const rowKey = this.rowKey;
const key = typeof rowKey === 'function' ? rowKey(record, index) : record[rowKey];
warning(
key !== undefined,
'Each record in table should have a unique `key` prop,' +
'or set `rowKey` to an unique primary key.',
);
return key === undefined ? index : key;
},
setScrollPosition(position) {
this.scrollPosition = position;
if (this.tableNode) {
const { prefixCls } = this;
if (position === 'both') {
classes(this.tableNode)
.remove(new RegExp(`^${prefixCls}-scroll-position-.+$`))
.add(`${prefixCls}-scroll-position-left`)
.add(`${prefixCls}-scroll-position-right`);
} else {
classes(this.tableNode)
.remove(new RegExp(`^${prefixCls}-scroll-position-.+$`))
.add(`${prefixCls}-scroll-position-${position}`);
}
}
},
setScrollPositionClassName() {
const node = this.ref_bodyTable;
const scrollToLeft = node.scrollLeft === 0;
const scrollToRight =
node.scrollLeft + 1 >=
node.children[0].getBoundingClientRect().width - node.getBoundingClientRect().width;
if (scrollToLeft && scrollToRight) {
this.setScrollPosition('both');
} else if (scrollToLeft) {
this.setScrollPosition('left');
} else if (scrollToRight) {
this.setScrollPosition('right');
} else if (this.scrollPosition !== 'middle') {
this.setScrollPosition('middle');
}
},
isTableLayoutFixed() {
const { tableLayout, columns = [], useFixedHeader, scroll = {} } = this.$props;
if (typeof tableLayout !== 'undefined') {
return tableLayout === 'fixed';
}
// if one column is ellipsis, use fixed table layout to fix align issue
if (columns.some(({ ellipsis }) => !!ellipsis)) {
return true;
}
// if header fixed, use fixed table layout to fix align issue
if (useFixedHeader || scroll.y) {
return true;
}
// if scroll.x is number/px/% width value, we should fixed table layout
// to avoid long word layout broken issue
if (scroll.x && scroll.x !== true && scroll.x !== 'max-content') {
return true;
}
return false;
},
handleWindowResize() {
this.syncFixedTableRowHeight();
this.setScrollPositionClassName();
},
syncFixedTableRowHeight() {
const tableRect = this.tableNode.getBoundingClientRect();
// If tableNode's height less than 0, suppose it is hidden and don't recalculate rowHeight.
// see: https://github.com/ant-design/ant-design/issues/4836
if (tableRect.height !== undefined && tableRect.height <= 0) {
return;
}
const { prefixCls } = this;
const headRows = this.ref_headTable
? this.ref_headTable.querySelectorAll('thead')
: this.ref_bodyTable.querySelectorAll('thead');
const bodyRows = this.ref_bodyTable.querySelectorAll(`.${prefixCls}-row`) || [];
const fixedColumnsHeadRowsHeight = [].map.call(headRows, row =>
row.getBoundingClientRect().height ? row.getBoundingClientRect().height - 0.5 : 'auto',
);
const state = this.store.getState();
const fixedColumnsBodyRowsHeight = [].reduce.call(
bodyRows,
(acc, row) => {
const rowKey = row.getAttribute('data-row-key');
const height =
row.getBoundingClientRect().height ||
state.fixedColumnsBodyRowsHeight[rowKey] ||
'auto';
acc[rowKey] = height;
return acc;
},
{},
);
if (
shallowequal(state.fixedColumnsHeadRowsHeight, fixedColumnsHeadRowsHeight) &&
shallowequal(state.fixedColumnsBodyRowsHeight, fixedColumnsBodyRowsHeight)
) {
return;
}
this.store.setState({
fixedColumnsHeadRowsHeight,
fixedColumnsBodyRowsHeight,
});
},
resetScrollX() {
if (this.ref_headTable) {
this.ref_headTable.scrollLeft = 0;
}
if (this.ref_bodyTable) {
this.ref_bodyTable.scrollLeft = 0;
}
},
hasScrollX() {
const { scroll = {} } = this;
return 'x' in scroll;
},
handleBodyScrollLeft(e) {
// Fix https://github.com/ant-design/ant-design/issues/7635
if (e.currentTarget !== e.target) {
return;
}
const target = e.target;
const { scroll = {} } = this;
const { ref_headTable, ref_bodyTable } = this;
if (target.scrollLeft !== this.lastScrollLeft && scroll.x) {
if (target === ref_bodyTable && ref_headTable) {
ref_headTable.scrollLeft = target.scrollLeft;
} else if (target === ref_headTable && ref_bodyTable) {
ref_bodyTable.scrollLeft = target.scrollLeft;
}
this.setScrollPositionClassName();
}
// Remember last scrollLeft for scroll direction detecting.
this.lastScrollLeft = target.scrollLeft;
},
handleBodyScrollTop(e) {
const target = e.target;
// Fix https://github.com/ant-design/ant-design/issues/9033
if (e.currentTarget !== target) {
return;
}
const { scroll = {} } = this;
const {
ref_headTable,
ref_bodyTable,
ref_fixedColumnsBodyLeft,
ref_fixedColumnsBodyRight,
} = this;
if (target.scrollTop !== this.lastScrollTop && scroll.y && target !== ref_headTable) {
const scrollTop = target.scrollTop;
if (ref_fixedColumnsBodyLeft && target !== ref_fixedColumnsBodyLeft) {
ref_fixedColumnsBodyLeft.scrollTop = scrollTop;
}
if (ref_fixedColumnsBodyRight && target !== ref_fixedColumnsBodyRight) {
ref_fixedColumnsBodyRight.scrollTop = scrollTop;
}
if (ref_bodyTable && target !== ref_bodyTable) {
ref_bodyTable.scrollTop = scrollTop;
}
}
// Remember last scrollTop for scroll direction detecting.
this.lastScrollTop = target.scrollTop;
},
handleBodyScroll(e) {
this.handleBodyScrollLeft(e);
this.handleBodyScrollTop(e);
},
handleWheel(event) {
const { scroll = {} } = this.$props;
if (window.navigator.userAgent.match(/Trident\/7\./) && scroll.y) {
event.preventDefault();
const wd = event.deltaY;
const target = event.target;
const {
ref_bodyTable: bodyTable,
ref_fixedColumnsBodyLeft: fixedColumnsBodyLeft,
ref_fixedColumnsBodyRight: fixedColumnsBodyRight,
} = this;
let scrollTop = 0;
if (this.lastScrollTop) {
scrollTop = this.lastScrollTop + wd;
} else {
scrollTop = wd;
}
if (fixedColumnsBodyLeft && target !== fixedColumnsBodyLeft) {
fixedColumnsBodyLeft.scrollTop = scrollTop;
}
if (fixedColumnsBodyRight && target !== fixedColumnsBodyRight) {
fixedColumnsBodyRight.scrollTop = scrollTop;
}
if (bodyTable && target !== bodyTable) {
bodyTable.scrollTop = scrollTop;
}
}
},
// saveChildrenRef(name, node) {
// this[`ref_${name}`] = node;
// },
saveRef(name) {
return node => {
this[`ref_${name}`] = node;
};
},
saveTableNodeRef(node) {
this.tableNode = node;
},
renderMainTable() {
const { scroll, prefixCls } = this;
const isAnyColumnsFixed = this.columnManager.isAnyColumnsFixed();
const scrollable = isAnyColumnsFixed || scroll.x || scroll.y;
const table = [
this.renderTable({
columns: this.columnManager.groupedColumns(),
isAnyColumnsFixed,
}),
this.renderEmptyText(),
this.renderFooter(),
];
return scrollable ? <div class={`${prefixCls}-scroll`}>{table}</div> : table;
},
renderLeftFixedTable() {
const { prefixCls } = this;
return (
<div class={`${prefixCls}-fixed-left`}>
{this.renderTable({
columns: this.columnManager.leftColumns(),
fixed: 'left',
})}
</div>
);
},
renderRightFixedTable() {
const { prefixCls } = this;
return (
<div class={`${prefixCls}-fixed-right`}>
{this.renderTable({
columns: this.columnManager.rightColumns(),
fixed: 'right',
})}
</div>
);
},
renderTable(options) {
const { columns, fixed, isAnyColumnsFixed } = options;
const { prefixCls, scroll = {} } = this;
const tableClassName = scroll.x || fixed ? `${prefixCls}-fixed` : '';
const headTable = (
<HeadTable
key="head"
columns={columns}
fixed={fixed}
tableClassName={tableClassName}
handleBodyScrollLeft={this.handleBodyScrollLeft}
expander={this.expander}
/>
);
const bodyTable = (
<BodyTable
key="body"
columns={columns}
fixed={fixed}
tableClassName={tableClassName}
getRowKey={this.getRowKey}
handleWheel={this.handleWheel}
handleBodyScroll={this.handleBodyScroll}
expander={this.expander}
isAnyColumnsFixed={isAnyColumnsFixed}
/>
);
return [headTable, bodyTable];
},
renderTitle() {
const { title, prefixCls, data } = this;
return title ? (
<div class={`${prefixCls}-title`} key="title">
{title(data)}
</div>
) : null;
},
renderFooter() {
const { footer, prefixCls, data } = this;
return footer ? (
<div class={`${prefixCls}-footer`} key="footer">
{footer(data)}
</div>
) : null;
},
renderEmptyText() {
const { emptyText, prefixCls, data } = this;
if (data.length) {
return null;
}
const emptyClassName = `${prefixCls}-placeholder`;
return (
<div class={emptyClassName} key="emptyText">
{typeof emptyText === 'function' ? emptyText() : emptyText}
</div>
);
},
},
render() {
const props = getOptionProps(this);
const { columnManager, getRowKey } = this;
const prefixCls = props.prefixCls;
const tableClassName = classNames(props.prefixCls, {
[`${prefixCls}-fixed-header`]: props.useFixedHeader || (props.scroll && props.scroll.y),
[`${prefixCls}-scroll-position-left ${prefixCls}-scroll-position-right`]:
this.scrollPosition === 'both',
[`${prefixCls}-scroll-position-${this.scrollPosition}`]: this.scrollPosition !== 'both',
[`${prefixCls}-layout-fixed`]: this.isTableLayoutFixed(),
});
const hasLeftFixed = columnManager.isAnyColumnsLeftFixed();
const hasRightFixed = columnManager.isAnyColumnsRightFixed();
const expandableTableProps = {
props: {
...props,
columnManager,
getRowKey,
},
on: getListeners(this),
scopedSlots: {
default: expander => {
this.expander = expander;
return (
<div
{...{
directives: [
{
name: 'ant-ref',
value: this.saveTableNodeRef,
},
],
}}
class={tableClassName}
// style={props.style}
// id={props.id}
>
{this.renderTitle()}
<div class={`${prefixCls}-content`}>
{this.renderMainTable()}
{hasLeftFixed && this.renderLeftFixedTable()}
{hasRightFixed && this.renderRightFixedTable()}
</div>
</div>
);
},
},
};
return (
<Provider store={this.store}>
<ExpandableTable {...expandableTableProps} />
</Provider>
);
},
};