484 lines
16 KiB
JavaScript
484 lines
16 KiB
JavaScript
import { arrayFindIndex } from 'element-ui/src/utils/util';
|
||
import { getCell, getColumnByCell, getRowIdentity } from './util';
|
||
import { getStyle, hasClass, removeClass, addClass } from 'element-ui/src/utils/dom';
|
||
import ElCheckbox from 'element-ui/packages/checkbox';
|
||
import ElTooltip from 'element-ui/packages/tooltip';
|
||
import debounce from 'throttle-debounce/debounce';
|
||
import LayoutObserver from './layout-observer';
|
||
import { mapStates } from './store/helper';
|
||
|
||
export default {
|
||
name: 'ElTableBody',
|
||
|
||
mixins: [LayoutObserver],
|
||
|
||
components: {
|
||
ElCheckbox,
|
||
ElTooltip
|
||
},
|
||
|
||
props: {
|
||
store: {
|
||
required: true
|
||
},
|
||
stripe: Boolean,
|
||
context: {},
|
||
rowClassName: [String, Function],
|
||
rowStyle: [Object, Function],
|
||
fixed: String,
|
||
highlight: Boolean
|
||
},
|
||
|
||
render(h) {
|
||
const data = this.data || [];
|
||
return (
|
||
<table
|
||
class="el-table__body"
|
||
cellspacing="0"
|
||
cellpadding="0"
|
||
border="0">
|
||
<colgroup>
|
||
{
|
||
this.columns.map(column => <col name={ column.id } key={column.id} />)
|
||
}
|
||
</colgroup>
|
||
<tbody>
|
||
{
|
||
data.reduce((acc, row) => {
|
||
return acc.concat(this.wrappedRowRender(row, acc.length));
|
||
}, [])
|
||
}
|
||
<el-tooltip effect={ this.table.tooltipEffect } placement="top" ref="tooltip" content={ this.tooltipContent }></el-tooltip>
|
||
</tbody>
|
||
</table>
|
||
);
|
||
},
|
||
|
||
computed: {
|
||
table() {
|
||
return this.$parent;
|
||
},
|
||
|
||
...mapStates({
|
||
data: 'data',
|
||
columns: 'columns',
|
||
treeIndent: 'indent',
|
||
leftFixedLeafCount: 'fixedLeafColumnsLength',
|
||
rightFixedLeafCount: 'rightFixedLeafColumnsLength',
|
||
columnsCount: states => states.columns.length,
|
||
leftFixedCount: states => states.fixedColumns.length,
|
||
rightFixedCount: states => states.rightFixedColumns.length,
|
||
hasExpandColumn: states => states.columns.some(({ type }) => type === 'expand')
|
||
}),
|
||
|
||
firstDefaultColumnIndex() {
|
||
return arrayFindIndex(this.columns, ({ type }) => type === 'default');
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
// don't trigger getter of currentRow in getCellClass. see https://jsfiddle.net/oe2b4hqt/
|
||
// update DOM manually. see https://github.com/ElemeFE/element/pull/13954/files#diff-9b450c00d0a9dec0ffad5a3176972e40
|
||
'store.states.hoverRow'(newVal, oldVal) {
|
||
if (!this.store.states.isComplex || this.$isServer) return;
|
||
let raf = window.requestAnimationFrame;
|
||
if (!raf) {
|
||
raf = (fn) => setTimeout(fn, 16);
|
||
}
|
||
raf(() => {
|
||
const rows = this.$el.querySelectorAll('.el-table__row');
|
||
const oldRow = rows[oldVal];
|
||
const newRow = rows[newVal];
|
||
if (oldRow) {
|
||
removeClass(oldRow, 'hover-row');
|
||
}
|
||
if (newRow) {
|
||
addClass(newRow, 'hover-row');
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
tooltipContent: ''
|
||
};
|
||
},
|
||
|
||
created() {
|
||
this.activateTooltip = debounce(50, tooltip => tooltip.handleShowPopper());
|
||
},
|
||
|
||
methods: {
|
||
getKeyOfRow(row, index) {
|
||
const rowKey = this.table.rowKey;
|
||
if (rowKey) {
|
||
return getRowIdentity(row, rowKey);
|
||
}
|
||
return index;
|
||
},
|
||
|
||
isColumnHidden(index) {
|
||
if (this.fixed === true || this.fixed === 'left') {
|
||
return index >= this.leftFixedLeafCount;
|
||
} else if (this.fixed === 'right') {
|
||
return index < this.columnsCount - this.rightFixedLeafCount;
|
||
} else {
|
||
return (index < this.leftFixedLeafCount) || (index >= this.columnsCount - this.rightFixedLeafCount);
|
||
}
|
||
},
|
||
|
||
getSpan(row, column, rowIndex, columnIndex) {
|
||
let rowspan = 1;
|
||
let colspan = 1;
|
||
const fn = this.table.spanMethod;
|
||
if (typeof fn === 'function') {
|
||
const result = fn({
|
||
row,
|
||
column,
|
||
rowIndex,
|
||
columnIndex
|
||
});
|
||
if (Array.isArray(result)) {
|
||
rowspan = result[0];
|
||
colspan = result[1];
|
||
} else if (typeof result === 'object') {
|
||
rowspan = result.rowspan;
|
||
colspan = result.colspan;
|
||
}
|
||
}
|
||
return { rowspan, colspan };
|
||
},
|
||
|
||
getRowStyle(row, rowIndex) {
|
||
const rowStyle = this.table.rowStyle;
|
||
if (typeof rowStyle === 'function') {
|
||
return rowStyle.call(null, {
|
||
row,
|
||
rowIndex
|
||
});
|
||
}
|
||
return rowStyle || null;
|
||
},
|
||
|
||
getRowClass(row, rowIndex) {
|
||
const classes = ['el-table__row'];
|
||
if (this.table.highlightCurrentRow && row === this.store.states.currentRow) {
|
||
classes.push('current-row');
|
||
}
|
||
|
||
if (this.stripe && rowIndex % 2 === 1) {
|
||
classes.push('el-table__row--striped');
|
||
}
|
||
const rowClassName = this.table.rowClassName;
|
||
if (typeof rowClassName === 'string') {
|
||
classes.push(rowClassName);
|
||
} else if (typeof rowClassName === 'function') {
|
||
classes.push(rowClassName.call(null, {
|
||
row,
|
||
rowIndex
|
||
}));
|
||
}
|
||
|
||
if (this.store.states.expandRows.indexOf(row) > -1) {
|
||
classes.push('expanded');
|
||
}
|
||
|
||
return classes;
|
||
},
|
||
|
||
getCellStyle(rowIndex, columnIndex, row, column) {
|
||
const cellStyle = this.table.cellStyle;
|
||
if (typeof cellStyle === 'function') {
|
||
return cellStyle.call(null, {
|
||
rowIndex,
|
||
columnIndex,
|
||
row,
|
||
column
|
||
});
|
||
}
|
||
return cellStyle;
|
||
},
|
||
|
||
getCellClass(rowIndex, columnIndex, row, column) {
|
||
const classes = [column.id, column.align, column.className];
|
||
|
||
if (this.isColumnHidden(columnIndex)) {
|
||
classes.push('is-hidden');
|
||
}
|
||
|
||
const cellClassName = this.table.cellClassName;
|
||
if (typeof cellClassName === 'string') {
|
||
classes.push(cellClassName);
|
||
} else if (typeof cellClassName === 'function') {
|
||
classes.push(cellClassName.call(null, {
|
||
rowIndex,
|
||
columnIndex,
|
||
row,
|
||
column
|
||
}));
|
||
}
|
||
|
||
return classes.join(' ');
|
||
},
|
||
|
||
getColspanRealWidth(columns, colspan, index) {
|
||
if (colspan < 1) {
|
||
return columns[index].realWidth;
|
||
}
|
||
const widthArr = columns.map(({ realWidth }) => realWidth).slice(index, index + colspan);
|
||
return widthArr.reduce((acc, width) => acc + width, -1);
|
||
},
|
||
|
||
handleCellMouseEnter(event, row) {
|
||
const table = this.table;
|
||
const cell = getCell(event);
|
||
|
||
if (cell) {
|
||
const column = getColumnByCell(table, cell);
|
||
const hoverState = table.hoverState = {cell, column, row};
|
||
table.$emit('cell-mouse-enter', hoverState.row, hoverState.column, hoverState.cell, event);
|
||
}
|
||
|
||
// 判断是否text-overflow, 如果是就显示tooltip
|
||
const cellChild = event.target.querySelector('.cell');
|
||
if (!(hasClass(cellChild, 'el-tooltip') && cellChild.childNodes.length)) {
|
||
return;
|
||
}
|
||
// use range width instead of scrollWidth to determine whether the text is overflowing
|
||
// to address a potential FireFox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1074543#c3
|
||
const range = document.createRange();
|
||
range.setStart(cellChild, 0);
|
||
range.setEnd(cellChild, cellChild.childNodes.length);
|
||
const rangeWidth = range.getBoundingClientRect().width;
|
||
const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
|
||
(parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
|
||
if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
|
||
const tooltip = this.$refs.tooltip;
|
||
// TODO 会引起整个 Table 的重新渲染,需要优化
|
||
this.tooltipContent = cell.innerText || cell.textContent;
|
||
tooltip.referenceElm = cell;
|
||
tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none');
|
||
tooltip.doDestroy();
|
||
tooltip.setExpectedState(true);
|
||
this.activateTooltip(tooltip);
|
||
}
|
||
},
|
||
|
||
handleCellMouseLeave(event) {
|
||
const tooltip = this.$refs.tooltip;
|
||
if (tooltip) {
|
||
tooltip.setExpectedState(false);
|
||
tooltip.handleClosePopper();
|
||
}
|
||
const cell = getCell(event);
|
||
if (!cell) return;
|
||
|
||
const oldHoverState = this.table.hoverState || {};
|
||
this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
|
||
},
|
||
|
||
handleMouseEnter: debounce(30, function(index) {
|
||
this.store.commit('setHoverRow', index);
|
||
}),
|
||
|
||
handleMouseLeave: debounce(30, function() {
|
||
this.store.commit('setHoverRow', null);
|
||
}),
|
||
|
||
handleContextMenu(event, row) {
|
||
this.handleEvent(event, row, 'contextmenu');
|
||
},
|
||
|
||
handleDoubleClick(event, row) {
|
||
this.handleEvent(event, row, 'dblclick');
|
||
},
|
||
|
||
handleClick(event, row) {
|
||
this.store.commit('setCurrentRow', row);
|
||
this.handleEvent(event, row, 'click');
|
||
},
|
||
|
||
handleEvent(event, row, name) {
|
||
const table = this.table;
|
||
const cell = getCell(event);
|
||
let column;
|
||
if (cell) {
|
||
column = getColumnByCell(table, cell);
|
||
if (column) {
|
||
table.$emit(`cell-${name}`, row, column, cell, event);
|
||
}
|
||
}
|
||
table.$emit(`row-${name}`, row, column, event);
|
||
},
|
||
|
||
rowRender(row, $index, treeRowData) {
|
||
const { treeIndent, columns, firstDefaultColumnIndex } = this;
|
||
const columnsHidden = columns.map((column, index) => this.isColumnHidden(index));
|
||
const rowClasses = this.getRowClass(row, $index);
|
||
let display = true;
|
||
if (treeRowData) {
|
||
rowClasses.push('el-table__row--level-' + treeRowData.level);
|
||
display = treeRowData.display;
|
||
}
|
||
// 指令 v-show 会覆盖 row-style 中 display
|
||
// 使用 :style 代替 v-show https://github.com/ElemeFE/element/issues/16995
|
||
let displayStyle = display ? null : {
|
||
display: 'none'
|
||
};
|
||
return (<tr
|
||
style={ [displayStyle, this.getRowStyle(row, $index)] }
|
||
class={ rowClasses }
|
||
key={ this.getKeyOfRow(row, $index) }
|
||
on-dblclick={ ($event) => this.handleDoubleClick($event, row) }
|
||
on-click={ ($event) => this.handleClick($event, row) }
|
||
on-contextmenu={ ($event) => this.handleContextMenu($event, row) }
|
||
on-mouseenter={ _ => this.handleMouseEnter($index) }
|
||
on-mouseleave={ this.handleMouseLeave }>
|
||
{
|
||
columns.map((column, cellIndex) => {
|
||
const { rowspan, colspan } = this.getSpan(row, column, $index, cellIndex);
|
||
if (!rowspan || !colspan) {
|
||
return null;
|
||
}
|
||
const columnData = { ...column };
|
||
columnData.realWidth = this.getColspanRealWidth(columns, colspan, cellIndex);
|
||
const data = {
|
||
store: this.store,
|
||
_self: this.context || this.table.$vnode.context,
|
||
column: columnData,
|
||
row,
|
||
$index
|
||
};
|
||
if (cellIndex === firstDefaultColumnIndex && treeRowData) {
|
||
data.treeNode = {
|
||
indent: treeRowData.level * treeIndent,
|
||
level: treeRowData.level
|
||
};
|
||
if (typeof treeRowData.expanded === 'boolean') {
|
||
data.treeNode.expanded = treeRowData.expanded;
|
||
// 表明是懒加载
|
||
if ('loading' in treeRowData) {
|
||
data.treeNode.loading = treeRowData.loading;
|
||
}
|
||
if ('noLazyChildren' in treeRowData) {
|
||
data.treeNode.noLazyChildren = treeRowData.noLazyChildren;
|
||
}
|
||
}
|
||
}
|
||
return (
|
||
<td
|
||
style={ this.getCellStyle($index, cellIndex, row, column) }
|
||
class={ this.getCellClass($index, cellIndex, row, column) }
|
||
rowspan={ rowspan }
|
||
colspan={ colspan }
|
||
on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
|
||
on-mouseleave={ this.handleCellMouseLeave }>
|
||
{
|
||
column.renderCell.call(
|
||
this._renderProxy,
|
||
this.$createElement,
|
||
data,
|
||
columnsHidden[cellIndex]
|
||
)
|
||
}
|
||
</td>
|
||
);
|
||
})
|
||
}
|
||
</tr>);
|
||
},
|
||
|
||
wrappedRowRender(row, $index) {
|
||
const store = this.store;
|
||
const { isRowExpanded, assertRowKey } = store;
|
||
const { treeData, lazyTreeNodeMap, childrenColumnName, rowKey } = store.states;
|
||
if (this.hasExpandColumn && isRowExpanded(row)) {
|
||
const renderExpanded = this.table.renderExpanded;
|
||
const tr = this.rowRender(row, $index);
|
||
if (!renderExpanded) {
|
||
console.error('[Element Error]renderExpanded is required.');
|
||
return tr;
|
||
}
|
||
// 使用二维数组,避免修改 $index
|
||
return [[
|
||
tr,
|
||
<tr key={'expanded-row__' + tr.key}>
|
||
<td colspan={ this.columnsCount } class="el-table__expanded-cell">
|
||
{ renderExpanded(this.$createElement, { row, $index, store: this.store }) }
|
||
</td>
|
||
</tr>]];
|
||
} else if (Object.keys(treeData).length) {
|
||
assertRowKey();
|
||
// TreeTable 时,rowKey 必须由用户设定,不使用 getKeyOfRow 计算
|
||
// 在调用 rowRender 函数时,仍然会计算 rowKey,不太好的操作
|
||
const key = getRowIdentity(row, rowKey);
|
||
let cur = treeData[key];
|
||
let treeRowData = null;
|
||
if (cur) {
|
||
treeRowData = {
|
||
expanded: cur.expanded,
|
||
level: cur.level,
|
||
display: true
|
||
};
|
||
if (typeof cur.lazy === 'boolean') {
|
||
if (typeof cur.loaded === 'boolean' && cur.loaded) {
|
||
treeRowData.noLazyChildren = !(cur.children && cur.children.length);
|
||
}
|
||
treeRowData.loading = cur.loading;
|
||
}
|
||
}
|
||
const tmp = [this.rowRender(row, $index, treeRowData)];
|
||
// 渲染嵌套数据
|
||
if (cur) {
|
||
// currentRow 记录的是 index,所以还需主动增加 TreeTable 的 index
|
||
let i = 0;
|
||
const traverse = (children, parent) => {
|
||
if (!(children && children.length && parent)) return;
|
||
children.forEach(node => {
|
||
// 父节点的 display 状态影响子节点的显示状态
|
||
const innerTreeRowData = {
|
||
display: parent.display && parent.expanded,
|
||
level: parent.level + 1
|
||
};
|
||
const childKey = getRowIdentity(node, rowKey);
|
||
if (childKey === undefined || childKey === null) {
|
||
throw new Error('for nested data item, row-key is required.');
|
||
}
|
||
cur = { ...treeData[childKey] };
|
||
// 对于当前节点,分成有无子节点两种情况。
|
||
// 如果包含子节点的,设置 expanded 属性。
|
||
// 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定。
|
||
if (cur) {
|
||
innerTreeRowData.expanded = cur.expanded;
|
||
// 懒加载的某些节点,level 未知
|
||
cur.level = cur.level || innerTreeRowData.level;
|
||
cur.display = !!(cur.expanded && innerTreeRowData.display);
|
||
if (typeof cur.lazy === 'boolean') {
|
||
if (typeof cur.loaded === 'boolean' && cur.loaded) {
|
||
innerTreeRowData.noLazyChildren = !(cur.children && cur.children.length);
|
||
}
|
||
innerTreeRowData.loading = cur.loading;
|
||
}
|
||
}
|
||
i++;
|
||
tmp.push(this.rowRender(node, $index + i, innerTreeRowData));
|
||
if (cur) {
|
||
const nodes = lazyTreeNodeMap[childKey] || node[childrenColumnName];
|
||
traverse(nodes, cur);
|
||
}
|
||
});
|
||
};
|
||
// 对于 root 节点,display 一定为 true
|
||
cur.display = true;
|
||
const nodes = lazyTreeNodeMap[key] || row[childrenColumnName];
|
||
traverse(nodes, cur);
|
||
}
|
||
return tmp;
|
||
} else {
|
||
return this.rowRender(row, $index);
|
||
}
|
||
}
|
||
}
|
||
};
|