refactor: [el-table] using sticky position refactor fixed column (#4414)

* refactor: [el-table] using sticky position refactor fixed column

re #4399

* fix: solve the misalignment issue

* fix: optimize code
This commit is contained in:
msidolphin
2022-01-05 15:13:01 +08:00
committed by GitHub
parent caa6982e47
commit e3e59afc75
15 changed files with 754 additions and 606 deletions

View File

@@ -66,25 +66,43 @@ describe('table column', () => {
it('fixed', async () => {
const wrapper = createTable(
'fixed label="test1"',
'fixed label="test1" width="100px"',
'fixed="right" label="test2"',
'fixed="left" label="test3"'
)
await nextTick()
const leftFixedHeaderColumns = wrapper.findAll(
'.el-table__header .el-table-fixed-column--left'
)
const leftFixedBodyColumns = wrapper.findAll(
'.el-table__body .el-table-fixed-column--left'
)
const rightFixedHeaderColumns = wrapper.findAll(
'.el-table__header .el-table-fixed-column--right'
)
const rightFixedBodyColumns = wrapper.findAll(
'.el-table__body .el-table-fixed-column--right'
)
expect(leftFixedHeaderColumns).toHaveLength(2)
expect(leftFixedBodyColumns).toHaveLength(10)
expect(rightFixedHeaderColumns).toHaveLength(1)
expect(rightFixedBodyColumns).toHaveLength(5)
expect(leftFixedHeaderColumns.at(0).text()).toBe('test1')
expect(leftFixedHeaderColumns.at(1).text()).toBe('test3')
expect(leftFixedHeaderColumns.at(1).classes()).toContain('is-last-column')
expect(rightFixedHeaderColumns.at(0).text()).toBe('test2')
expect(rightFixedHeaderColumns.at(0).classes()).toContain(
'is-first-column'
)
expect(getComputedStyle(leftFixedHeaderColumns.at(0).element).left).toBe(
'0px'
)
expect(getComputedStyle(leftFixedHeaderColumns.at(1).element).left).toBe(
'100px'
)
expect(
wrapper
.findAll('.el-table__fixed th:not(.is-hidden)')
.map((node) => node.text())
).toEqual(['test1', 'test3'])
expect(
wrapper
.findAll('.el-table__fixed-right th:not(.is-hidden)')
.map((node) => node.text())
).toEqual(['test2'])
expect(
wrapper.find('.el-table__body-wrapper').attributes('style')
).toBeFalsy()
getComputedStyle(rightFixedHeaderColumns.at(0).element).right
).toBe('0px')
wrapper.unmount()
})
@@ -1017,12 +1035,10 @@ describe('table column', () => {
})
await nextTick()
expect(Object.keys(wrapper.find('.el-table__fixed')).length).toEqual(0)
expect(wrapper.find('.el-table-fixed-column--left').exists()).toBeFalsy()
wrapper.vm.fixed = true
await nextTick()
expect(
Object.keys(wrapper.find('.el-table__fixed')).length
).toBeGreaterThan(0)
expect(wrapper.find('.el-table-fixed-column--left').exists()).toBeTruthy()
wrapper.unmount()
})

View File

@@ -63,7 +63,9 @@ export default defineComponent({
}
},
render() {
const data = this.store.states.data.value || []
const { wrappedRowRender, store } = this
const data = store.states.data.value || []
const columns = store.states.columns.value
return h(
'table',
{
@@ -73,10 +75,10 @@ export default defineComponent({
border: '0',
},
[
hColgroup(this.store.states.columns.value),
hColgroup(columns),
h('tbody', {}, [
data.reduce((acc: VNode[], row) => {
return acc.concat(this.wrappedRowRender(row, acc.length))
return acc.concat(wrappedRowRender(row, acc.length))
}, []),
]),
]

View File

@@ -1,5 +1,9 @@
import { getCurrentInstance } from 'vue'
import {
getFixedColumnOffset,
getFixedColumnsClass,
ensurePosition,
} from '../util'
import type { TableColumnCtx } from '../table-column/defaults'
import type { Table } from '../table/defaults'
import type { TableBodyProps } from './defaults'
@@ -7,24 +11,7 @@ import type { TableBodyProps } from './defaults'
function useStyles<T>(props: Partial<TableBodyProps<T>>) {
const instance = getCurrentInstance()
const parent = instance.parent as Table<T>
const isColumnHidden = (index) => {
if (props.fixed === 'left') {
return index >= props.store.states.fixedLeafColumnsLength.value
} else if (props.fixed === 'right') {
return (
index <
props.store.states.columns.value.length -
props.store.states.rightFixedLeafColumnsLength.value
)
} else {
return (
index < props.store.states.fixedLeafColumnsLength.value ||
index >=
props.store.states.columns.value.length -
props.store.states.rightFixedLeafColumnsLength.value
)
}
}
const getRowStyle = (row: T, rowIndex: number) => {
const rowStyle = parent.props.rowStyle
if (typeof rowStyle === 'function') {
@@ -74,15 +61,23 @@ function useStyles<T>(props: Partial<TableBodyProps<T>>) {
column: TableColumnCtx<T>
) => {
const cellStyle = parent.props.cellStyle
let cellStyles = cellStyle ?? {}
if (typeof cellStyle === 'function') {
return cellStyle.call(null, {
cellStyles = cellStyle.call(null, {
rowIndex,
columnIndex,
row,
column,
})
}
return cellStyle
const fixedStyle = getFixedColumnOffset(
columnIndex,
props.fixed,
props.store
)
ensurePosition(fixedStyle, 'left')
ensurePosition(fixedStyle, 'right')
return Object.assign({}, cellStyles, fixedStyle)
}
const getCellClass = (
@@ -91,12 +86,10 @@ function useStyles<T>(props: Partial<TableBodyProps<T>>) {
row: T,
column: TableColumnCtx<T>
) => {
const classes = [column.id, column.align, column.className]
if (isColumnHidden(columnIndex)) {
classes.push('is-hidden')
}
const fixedClasses = column.isSubColumn
? []
: getFixedColumnsClass(columnIndex, props.fixed, props.store)
const classes = [column.id, column.align, column.className, ...fixedClasses]
const cellClassName = parent.props.cellClassName
if (typeof cellClassName === 'string') {
classes.push(cellClassName)
@@ -164,7 +157,6 @@ function useStyles<T>(props: Partial<TableBodyProps<T>>) {
getCellClass,
getSpan,
getColspanRealWidth,
isColumnHidden,
}
}

View File

@@ -58,6 +58,7 @@ interface TableColumnCtx<T> {
filterable: boolean | FilterMethods<T> | Filters
order: string
isColumnGroup: boolean
isSubColumn: boolean
columns: TableColumnCtx<T>[]
getColumnIndex: () => number
no: number

View File

@@ -78,6 +78,7 @@ export default defineComponent({
filteredValue: [],
filterPlacement: '',
isColumnGroup: false,
isSubColumn: false,
filterOpened: false,
// sort 相关属性
sortable,

View File

@@ -1,5 +1,5 @@
import { defineComponent, h } from 'vue'
import { hGutter, hColgroup } from '../h-helper'
import { hColgroup } from '../h-helper'
import useStyle from './style-helper'
import type { Store } from '../store'
@@ -42,31 +42,40 @@ export default defineComponent({
},
},
setup(props) {
const { hasGutter, getRowClasses, columns } = useStyle(
props as TableFooter<DefaultRow>
)
const { hasGutter, getCellClasses, getCellStyles, columns, gutterWidth } =
useStyle(props as TableFooter<DefaultRow>)
return {
getRowClasses,
getCellClasses,
getCellStyles,
hasGutter,
gutterWidth,
columns,
}
},
render() {
const {
hasGutter,
gutterWidth,
columns,
getCellStyles,
getCellClasses,
summaryMethod,
sumText,
} = this
const data = this.store.states.data.value
let sums = []
if (this.summaryMethod) {
sums = this.summaryMethod({
columns: this.columns,
data: this.store.states.data.value,
if (summaryMethod) {
sums = summaryMethod({
columns,
data,
})
} else {
this.columns.forEach((column, index) => {
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = this.sumText
sums[index] = sumText
return
}
const values = this.store.states.data.value.map((item) =>
Number(item[column.property])
)
const values = data.map((item) => Number(item[column.property]))
const precisions = []
let notNumber = true
values.forEach((value) => {
@@ -100,25 +109,23 @@ export default defineComponent({
border: '0',
},
[
hColgroup(this.columns, this.hasGutter),
hColgroup(columns, hasGutter),
h(
'tbody',
{
class: [{ 'has-gutter': this.hasGutter }],
class: [{ 'has-gutter': hasGutter }],
},
[
h('tr', {}, [
...this.columns.map((column, cellIndex) =>
...columns.map((column, cellIndex) =>
h(
'td',
{
key: cellIndex,
colspan: column.colSpan,
rowspan: column.rowSpan,
class: [
...this.getRowClasses(column, cellIndex),
'el-table__cell',
],
class: getCellClasses(columns, cellIndex, hasGutter),
style: getCellStyles(column, cellIndex, hasGutter),
},
[
h(
@@ -131,7 +138,13 @@ export default defineComponent({
]
)
),
this.hasGutter && hGutter(),
hasGutter &&
h('td', {
class: 'el-table__fixed-right-patch el-table__cell',
style: {
width: `${gutterWidth}px`,
},
}),
]),
]
),

View File

@@ -1,6 +1,11 @@
import { computed, getCurrentInstance } from 'vue'
import {
getFixedColumnOffset,
getFixedColumnsClass,
ensurePosition,
ensureRightFixedStyle,
} from '../util'
import useMapState from './mapState-helper'
import type { Table } from '../table/defaults'
import type { TableColumnCtx } from '../table-column/defaults'
import type { TableFooter } from '.'
@@ -8,60 +13,67 @@ import type { TableFooter } from '.'
function useStyle<T>(props: TableFooter<T>) {
const instance = getCurrentInstance()
const table = instance.parent as Table<T>
const store = table.store
const {
leftFixedLeafCount,
rightFixedLeafCount,
columnsCount,
leftFixedCount,
rightFixedCount,
columns,
} = useMapState<T>()
const { columns } = useMapState<T>()
const hasGutter = computed(() => {
return !props.fixed && !table.layout.gutterWidth
return (
!props.fixed &&
table.layout.gutterWidth > 0 &&
table.layout.height.value &&
table.layout.bodyScrollHeight.value > table.layout.bodyHeight.value
)
})
const isCellHidden = (
index: number,
const gutterWidth = computed(() => {
return table.layout.gutterWidth
})
const getCellClasses = (
columns: TableColumnCtx<T>[],
column: TableColumnCtx<T>
cellIndex: number,
hasGutter: boolean
) => {
if (props.fixed || props.fixed === 'left') {
return index >= leftFixedLeafCount.value
} else if (props.fixed === 'right') {
let before = 0
for (let i = 0; i < index; i++) {
before += columns[i].colSpan
}
return before < columnsCount.value - rightFixedLeafCount.value
} else if (!props.fixed && column.fixed) {
// hide cell when footer instance is not fixed and column is fixed
return true
} else {
return (
index < leftFixedCount.value ||
index >= columnsCount.value - rightFixedCount.value
)
}
}
const getRowClasses = (column: TableColumnCtx<T>, cellIndex: number) => {
const classes = [column.id, column.align, column.labelClassName]
const column = columns[cellIndex]
const classes = [
'el-table__cell',
column.id,
column.align,
column.labelClassName,
...getFixedColumnsClass(cellIndex, column.fixed, props.store),
]
if (column.className) {
classes.push(column.className)
}
if (isCellHidden(cellIndex, store.states.columns.value, column)) {
classes.push('is-hidden')
}
if (!column.children) {
classes.push('is-leaf')
}
if (hasGutter && cellIndex === columns.length - 1) {
classes.push('last')
}
return classes
}
const getCellStyles = (
column: TableColumnCtx<T>,
cellIndex: number,
hasGutter: boolean
) => {
const fixedStyle = getFixedColumnOffset(
cellIndex,
column.fixed,
props.store
)
ensureRightFixedStyle(fixedStyle, hasGutter)
ensurePosition(fixedStyle, 'left')
ensurePosition(fixedStyle, 'right')
return fixedStyle
}
return {
hasGutter,
getRowClasses,
gutterWidth,
getCellClasses,
getCellStyles,
columns,
}
}

View File

@@ -65,7 +65,14 @@ export default defineComponent({
const { tableLayout, onColumnsChange, onScrollableChange } =
useLayoutObserver(parent)
const hasGutter = computed(() => {
return !props.fixed && tableLayout.gutterWidth
return (
!props.fixed &&
tableLayout.gutterWidth > 0 &&
tableLayout.bodyScrollHeight.value > tableLayout.bodyHeight.value
)
})
const gutterWidth = computed(() => {
return tableLayout.gutterWidth
})
onMounted(() => {
nextTick(() => {
@@ -104,6 +111,7 @@ export default defineComponent({
columns: storeData.columns,
filterPanels,
hasGutter,
gutterWidth,
onColumnsChange,
onScrollableChange,
columnRows,
@@ -123,6 +131,26 @@ export default defineComponent({
}
},
render() {
const {
columns,
isGroup,
hasGutter,
columnRows,
gutterWidth,
getHeaderCellStyle,
getHeaderCellClass,
getHeaderRowClass,
getHeaderRowStyle,
handleHeaderClick,
handleHeaderContextMenu,
handleMouseDown,
handleMouseMove,
handleSortClick,
handleMouseOut,
store,
$parent,
} = this
let rowSpan = 1
return h(
'table',
{
@@ -132,113 +160,135 @@ export default defineComponent({
class: 'el-table__header',
},
[
hColgroup(this.columns, this.hasGutter),
hColgroup(columns, hasGutter),
h(
'thead',
{
class: { 'is-group': this.isGroup, 'has-gutter': this.hasGutter },
class: { 'is-group': isGroup, 'has-gutter': hasGutter },
},
this.columnRows.map((subColumns, rowIndex) =>
columnRows.map((subColumns, rowIndex) =>
h(
'tr',
{
class: this.getHeaderRowClass(rowIndex),
class: getHeaderRowClass(rowIndex),
key: rowIndex,
style: this.getHeaderRowStyle(rowIndex),
style: getHeaderRowStyle(rowIndex),
},
subColumns.map((column, cellIndex) =>
h(
'th',
{
class: this.getHeaderCellClass(
rowIndex,
cellIndex,
subColumns,
column
),
colspan: column.colSpan,
key: `${column.id}-thead`,
rowSpan: column.rowSpan,
style: this.getHeaderCellStyle(
rowIndex,
cellIndex,
subColumns,
column
),
onClick: ($event) => this.handleHeaderClick($event, column),
onContextmenu: ($event) =>
this.handleHeaderContextMenu($event, column),
onMousedown: ($event) =>
this.handleMouseDown($event, column),
onMousemove: ($event) =>
this.handleMouseMove($event, column),
onMouseout: this.handleMouseOut,
},
[
h(
'div',
{
class: [
'cell',
column.filteredValue &&
column.filteredValue.length > 0
? 'highlight'
: '',
column.labelClassName,
],
},
[
column.renderHeader
? column.renderHeader({
subColumns
.map((column, cellIndex) => {
if (column.rowSpan > rowSpan) {
rowSpan = column.rowSpan
}
return h(
'th',
{
class: getHeaderCellClass(
rowIndex,
cellIndex,
subColumns,
column
),
colspan: column.colSpan,
key: `${column.id}-thead`,
rowSpan: column.rowSpan,
style: getHeaderCellStyle(
rowIndex,
cellIndex,
subColumns,
column,
hasGutter
),
onClick: ($event) => handleHeaderClick($event, column),
onContextmenu: ($event) =>
handleHeaderContextMenu($event, column),
onMousedown: ($event) => handleMouseDown($event, column),
onMousemove: ($event) => handleMouseMove($event, column),
onMouseout: handleMouseOut,
},
[
h(
'div',
{
class: [
'cell',
column.filteredValue &&
column.filteredValue.length > 0
? 'highlight'
: '',
column.labelClassName,
],
},
[
column.renderHeader
? column.renderHeader({
column,
$index: cellIndex,
store,
_self: $parent,
})
: column.label,
column.sortable &&
h(
'span',
{
onClick: ($event) =>
handleSortClick($event, column),
class: 'caret-wrapper',
},
[
h('i', {
onClick: ($event) =>
handleSortClick(
$event,
column,
'ascending'
),
class: 'sort-caret ascending',
}),
h('i', {
onClick: ($event) =>
handleSortClick(
$event,
column,
'descending'
),
class: 'sort-caret descending',
}),
]
),
column.filterable &&
h(FilterPanel, {
store: $parent.store,
placement:
column.filterPlacement || 'bottom-start',
column,
$index: cellIndex,
store: this.store,
_self: this.$parent,
})
: column.label,
column.sortable &&
h(
'span',
{
onClick: ($event) =>
this.handleSortClick($event, column),
class: 'caret-wrapper',
upDataColumn: (key, value) => {
column[key] = value
},
}),
]
),
]
)
})
.concat(
hasGutter && rowIndex === 0
? [
h(
'th',
{
class: 'el-table__fixed-right-patch el-table__cell',
key: `el-table--scrollbar`,
rowSpan,
style: {
width: `${gutterWidth}px`,
},
[
h('i', {
onClick: ($event) =>
this.handleSortClick(
$event,
column,
'ascending'
),
class: 'sort-caret ascending',
}),
h('i', {
onClick: ($event) =>
this.handleSortClick(
$event,
column,
'descending'
),
class: 'sort-caret descending',
}),
]
),
column.filterable &&
h(FilterPanel, {
store: this.$parent.store,
placement: column.filterPlacement || 'bottom-start',
column,
upDataColumn: (key, value) => {
column[key] = value
},
}),
},
[]
),
]
),
]
: []
)
)
)
)
),

View File

@@ -1,5 +1,10 @@
import { getCurrentInstance } from 'vue'
import {
getFixedColumnsClass,
getFixedColumnOffset,
ensurePosition,
ensureRightFixedStyle,
} from '../util'
import type { TableColumnCtx } from '../table-column/defaults'
import type { Table } from '../table/defaults'
import type { TableHeaderProps } from '.'
@@ -7,33 +12,6 @@ import type { TableHeaderProps } from '.'
function useStyle<T>(props: TableHeaderProps<T>) {
const instance = getCurrentInstance()
const parent = instance.parent as Table<T>
const storeData = parent.store.states
const isCellHidden = (
index: number,
columns: TableColumnCtx<T>[]
): boolean => {
let start = 0
for (let i = 0; i < index; i++) {
start += columns[i].colSpan
}
const after = start + columns[index].colSpan - 1
if (props.fixed === 'left') {
return after >= storeData.fixedLeafColumnsLength.value
} else if (props.fixed === 'right') {
return (
start <
storeData.columns.value.length -
storeData.rightFixedLeafColumnsLength.value
)
} else {
return (
after < storeData.fixedLeafColumnsLength.value ||
start >=
storeData.columns.value.length -
storeData.rightFixedLeafColumnsLength.value
)
}
}
const getHeaderRowStyle = (rowIndex: number) => {
const headerRowStyle = parent.props.headerRowStyle
@@ -59,18 +37,28 @@ function useStyle<T>(props: TableHeaderProps<T>) {
rowIndex: number,
columnIndex: number,
row: T,
column: TableColumnCtx<T>
column: TableColumnCtx<T>,
hasGutter: boolean
) => {
const headerCellStyle = parent.props.headerCellStyle
if (typeof headerCellStyle === 'function') {
return headerCellStyle.call(null, {
let headerCellStyles = parent.props.headerCellStyle ?? {}
if (typeof headerCellStyles === 'function') {
headerCellStyles = headerCellStyles.call(null, {
rowIndex,
columnIndex,
row,
column,
})
}
return headerCellStyle
const fixedStyle = getFixedColumnOffset<T>(
columnIndex,
column.fixed,
props.store,
row as unknown as TableColumnCtx<T>[]
)
ensureRightFixedStyle(fixedStyle, hasGutter)
ensurePosition(fixedStyle, 'left')
ensurePosition(fixedStyle, 'right')
return Object.assign({}, headerCellStyles, fixedStyle)
}
const getHeaderCellClass = (
@@ -79,19 +67,22 @@ function useStyle<T>(props: TableHeaderProps<T>) {
row: T,
column: TableColumnCtx<T>
) => {
const fixedClasses = column.isSubColumn
? []
: getFixedColumnsClass<T>(
columnIndex,
column.fixed,
props.store,
row as unknown as TableColumnCtx<T>[]
)
const classes = [
column.id,
column.order,
column.headerAlign,
column.className,
column.labelClassName,
...fixedClasses,
]
if (
rowIndex === 0 &&
isCellHidden(columnIndex, row as unknown as TableColumnCtx<T>[])
) {
classes.push('is-hidden')
}
if (!column.children) {
classes.push('is-leaf')

View File

@@ -60,6 +60,7 @@ const convertToRows = <T>(
column.rowSpan = maxLevel - column.level + 1
} else {
column.rowSpan = 1
column.children.forEach((col) => (col.isSubColumn = true))
}
rows[column.level - 1].push(column)
})

View File

@@ -29,6 +29,7 @@ class TableLayout<T> {
footerHeight: Ref<null | number> // Table Footer Height
viewportHeight: Ref<null | number> // Table Height - Scroll Bar Height
bodyHeight: Ref<null | number> // Table Height - Table Header Height
bodyScrollHeight: Ref<number>
fixedBodyHeight: Ref<null | number> // Table Height - Table Header Height - Scroll Bar Height
gutterWidth: number
constructor(options: Record<string, any>) {
@@ -50,6 +51,7 @@ class TableLayout<T> {
this.footerHeight = ref(44)
this.viewportHeight = ref(null)
this.bodyHeight = ref(null)
this.bodyScrollHeight = ref(0)
this.fixedBodyHeight = ref(null)
this.gutterWidth = scrollbarWidth()
for (const name in options) {
@@ -136,7 +138,8 @@ class TableLayout<T> {
updateElsHeight() {
if (!this.table.$ready) return nextTick(() => this.updateElsHeight())
const { headerWrapper, appendWrapper, footerWrapper } = this.table.refs
const { headerWrapper, appendWrapper, footerWrapper, bodyWrapper } =
this.table.refs
this.appendHeight.value = appendWrapper ? appendWrapper.offsetHeight : 0
if (this.showHeader && !headerWrapper) return
@@ -163,8 +166,13 @@ class TableLayout<T> {
? footerWrapper.offsetHeight
: 0)
if (this.height.value !== null) {
if (this.bodyHeight.value === null) {
requestAnimationFrame(() => this.updateElsHeight())
}
this.bodyHeight.value =
tableHeight - headerHeight - footerHeight + (footerWrapper ? 1 : 0)
this.bodyScrollHeight.value =
bodyWrapper.querySelector('.el-table__body')?.scrollHeight!
}
this.fixedBodyHeight.value = this.scrollX.value
? this.bodyHeight.value - this.gutterWidth

View File

@@ -1,5 +1,6 @@
<template>
<div
ref="tableWrapper"
:class="[
{
'el-table--fit': fit,
@@ -22,56 +23,62 @@
:style="style"
@mouseleave="handleMouseLeave()"
>
<div ref="hiddenColumns" class="hidden-columns">
<slot></slot>
</div>
<div
v-if="showHeader"
ref="headerWrapper"
v-mousewheel="handleHeaderFooterMousewheel"
class="el-table__header-wrapper"
>
<table-header
ref="tableHeader"
:border="border"
:default-sort="defaultSort"
:store="store"
:style="{
width: layout.bodyWidth.value ? layout.bodyWidth.value + 'px' : '',
}"
@set-drag-visible="setDragVisible"
/>
</div>
<div ref="bodyWrapper" :style="[bodyHeight]" class="el-table__body-wrapper">
<table-body
:context="context"
:highlight="highlightCurrentRow"
:row-class-name="rowClassName"
:tooltip-effect="tooltipEffect"
:row-style="rowStyle"
:store="store"
:stripe="stripe"
:style="{
width: bodyWidth,
}"
/>
<div
v-if="isEmpty"
ref="emptyBlock"
:style="emptyBlockStyle"
class="el-table__empty-block"
>
<span class="el-table__empty-text">
<slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot>
</span>
<div class="el-table__inner-wrapper">
<div ref="hiddenColumns" class="hidden-columns">
<slot></slot>
</div>
<div
v-if="$slots.append"
ref="appendWrapper"
class="el-table__append-wrapper"
v-if="showHeader"
ref="headerWrapper"
v-mousewheel="handleHeaderFooterMousewheel"
class="el-table__header-wrapper"
>
<slot name="append"></slot>
<table-header
ref="tableHeader"
:border="border"
:default-sort="defaultSort"
:store="store"
:style="tableBodyStyles"
@set-drag-visible="setDragVisible"
/>
</div>
<div ref="bodyWrapper" :style="bodyHeight" class="el-table__body-wrapper">
<table-body
:context="context"
:highlight="highlightCurrentRow"
:row-class-name="rowClassName"
:tooltip-effect="tooltipEffect"
:row-style="rowStyle"
:store="store"
:stripe="stripe"
:style="{
width: bodyWidth,
}"
/>
<div
v-if="isEmpty"
ref="emptyBlock"
:style="emptyBlockStyle"
class="el-table__empty-block"
>
<span class="el-table__empty-text">
<slot name="empty">{{ computedEmptyText }}</slot>
</span>
</div>
<div
v-if="$slots.append"
ref="appendWrapper"
class="el-table__append-wrapper"
>
<slot name="append"></slot>
</div>
</div>
<div v-if="border || isGroup" class="el-table__border-left-patch"></div>
<div
v-if="layout.scrollX.value && layout.height.value"
class="el-table__border-bottom-patch"
:style="borderBottomPatchStyles"
></div>
</div>
<div
v-if="showSummary"
@@ -84,170 +91,11 @@
:border="border"
:default-sort="defaultSort"
:store="store"
:style="{
width: layout.bodyWidth.value ? layout.bodyWidth.value + 'px' : '',
}"
:sum-text="sumText || t('el.table.sumText')"
:style="tableBodyStyles"
:sum-text="computedSumText"
:summary-method="summaryMethod"
/>
</div>
<div
v-if="store.states.fixedColumns.value.length > 0"
ref="fixedWrapper"
v-mousewheel="handleFixedMousewheel"
:style="[
{
width: layout.fixedWidth.value ? layout.fixedWidth.value + 'px' : '',
},
fixedHeight,
]"
class="el-table__fixed"
>
<div
v-if="showHeader"
ref="fixedHeaderWrapper"
class="el-table__fixed-header-wrapper"
>
<table-header
ref="fixedTableHeader"
:border="border"
:store="store"
:style="{
width: bodyWidth,
}"
fixed="left"
@set-drag-visible="setDragVisible"
/>
</div>
<div
ref="fixedBodyWrapper"
:style="[
{
top: layout.headerHeight.value + 'px',
},
fixedBodyHeight,
]"
class="el-table__fixed-body-wrapper"
>
<table-body
:highlight="highlightCurrentRow"
:row-class-name="rowClassName"
:tooltip-effect="tooltipEffect"
:row-style="rowStyle"
:store="store"
:stripe="stripe"
:style="{
width: bodyWidth,
}"
fixed="left"
/>
<div
v-if="$slots.append"
:style="{ height: layout.appendHeight.value + 'px' }"
class="el-table__append-gutter"
></div>
</div>
<div
v-if="showSummary"
v-show="!isEmpty"
ref="fixedFooterWrapper"
class="el-table__fixed-footer-wrapper"
>
<table-footer
:border="border"
:store="store"
:style="{
width: bodyWidth,
}"
:sum-text="sumText || t('el.table.sumText')"
:summary-method="summaryMethod"
fixed="left"
/>
</div>
</div>
<div
v-if="store.states.rightFixedColumns.value.length > 0"
ref="rightFixedWrapper"
v-mousewheel="handleFixedMousewheel"
:style="[
{
width: layout.rightFixedWidth.value
? layout.rightFixedWidth.value + 'px'
: '',
right: layout.scrollY.value
? (border ? layout.gutterWidth : layout.gutterWidth || 0) + 'px'
: '',
},
fixedHeight,
]"
class="el-table__fixed-right"
>
<div
v-if="showHeader"
ref="rightFixedHeaderWrapper"
class="el-table__fixed-header-wrapper"
>
<table-header
ref="rightFixedTableHeader"
:border="border"
:store="store"
:style="{
width: bodyWidth,
}"
fixed="right"
@set-drag-visible="setDragVisible"
/>
</div>
<div
ref="rightFixedBodyWrapper"
:style="[{ top: layout.headerHeight.value + 'px' }, fixedBodyHeight]"
class="el-table__fixed-body-wrapper"
>
<table-body
:highlight="highlightCurrentRow"
:row-class-name="rowClassName"
:tooltip-effect="tooltipEffect"
:row-style="rowStyle"
:store="store"
:stripe="stripe"
:style="{
width: bodyWidth,
}"
fixed="right"
/>
<div
v-if="$slots.append"
:style="{ height: layout.appendHeight.value + 'px' }"
class="el-table__append-gutter"
></div>
</div>
<div
v-if="showSummary"
v-show="!isEmpty"
ref="rightFixedFooterWrapper"
class="el-table__fixed-footer-wrapper"
>
<table-footer
:border="border"
:store="store"
:style="{
width: bodyWidth,
}"
:sum-text="sumText || t('el.table.sumText')"
:summary-method="summaryMethod"
fixed="right"
/>
</div>
</div>
<div
v-if="store.states.rightFixedColumns.value.length > 0"
ref="rightFixedPatch"
:style="{
width: layout.scrollY.value ? layout.gutterWidth + 'px' : '0',
height: layout.headerHeight.value + 'px',
}"
class="el-table__fixed-right-patch"
></div>
<div
v-show="resizeProxyVisible"
ref="resizeProxy"
@@ -350,6 +198,8 @@ export default defineComponent({
bodyWidth,
resizeState,
doLayout,
tableBodyStyles,
borderBottomPatchStyles,
} = useStyle<Row>(props, layout, store, table)
const debouncedUpdateLayout = debounce(doLayout, 50)
@@ -362,6 +212,14 @@ export default defineComponent({
doLayout,
debouncedUpdateLayout,
}
const computedSumText = computed(
() => props.sumText || t('el.table.sumText')
)
const computedEmptyText = computed(() => {
return props.emptyText || t('el.table.emptyText')
})
return {
layout,
store,
@@ -377,7 +235,9 @@ export default defineComponent({
isGroup,
bodyWidth,
bodyHeight,
tableBodyStyles,
emptyBlockStyle,
borderBottomPatchStyles,
debouncedUpdateLayout,
handleFixedMousewheel,
fixedHeight,
@@ -394,6 +254,8 @@ export default defineComponent({
t,
setDragVisible,
context: table,
computedSumText,
computedEmptyText,
}
},
})

View File

@@ -8,7 +8,6 @@ import {
unref,
nextTick,
} from 'vue'
import throttle from 'lodash/throttle'
import {
addResizeListener,
removeResizeListener,
@@ -94,6 +93,19 @@ function useStyle<T>(
)
})
const tableBodyStyles = computed(() => {
return {
width: layout.bodyWidth.value ? `${layout.bodyWidth.value}px` : '',
}
})
const borderBottomPatchStyles = computed(() => {
return {
bottom: `${layout.gutterWidth}px`,
right: `${layout.gutterWidth}px`,
}
})
const doLayout = () => {
if (shouldUpdateHeight.value) {
layout.updateElsHeight()
@@ -134,23 +146,15 @@ function useStyle<T>(
el.className = classList.join(' ')
}
const setScrollClass = (className: string) => {
const { bodyWrapper } = table.refs
setScrollClassByEl(bodyWrapper, className)
const { tableWrapper } = table.refs
setScrollClassByEl(tableWrapper, className)
}
const syncPostion = throttle(function () {
const syncPostion = function () {
if (!table.refs.bodyWrapper) return
const { scrollLeft, scrollTop, offsetWidth, scrollWidth } =
table.refs.bodyWrapper
const {
headerWrapper,
footerWrapper,
fixedBodyWrapper,
rightFixedBodyWrapper,
} = table.refs
const { scrollLeft, offsetWidth, scrollWidth } = table.refs.bodyWrapper
const { headerWrapper, footerWrapper } = table.refs
if (headerWrapper) headerWrapper.scrollLeft = scrollLeft
if (footerWrapper) footerWrapper.scrollLeft = scrollLeft
if (fixedBodyWrapper) fixedBodyWrapper.scrollTop = scrollTop
if (rightFixedBodyWrapper) rightFixedBodyWrapper.scrollTop = scrollTop
const maxScrollLeftPosition = scrollWidth - offsetWidth - 1
if (scrollLeft >= maxScrollLeftPosition) {
setScrollClass('is-scrolling-right')
@@ -159,7 +163,7 @@ function useStyle<T>(
} else {
setScrollClass('is-scrolling-middle')
}
}, 10)
}
const bindEvents = () => {
table.refs.bodyWrapper.addEventListener('scroll', syncPostion, {
@@ -335,6 +339,8 @@ function useStyle<T>(
bodyWidth,
resizeState,
doLayout,
tableBodyStyles,
borderBottomPatchStyles,
}
}

View File

@@ -2,6 +2,7 @@ import { hasOwn } from '@vue/shared'
import { createPopper } from '@popperjs/core'
import { PopupManager } from '@element-plus/utils/popup-manager'
import { getValueByPath } from '@element-plus/utils/util'
import scrollbarWidth from '@element-plus/utils/scrollbar-width'
import { off, on } from '@element-plus/utils/dom'
import type {
@@ -370,3 +371,143 @@ export function createTablePopper(
on(trigger, 'mouseleave', removePopper)
return popperInstance
}
export const isFixedColumn = <T>(
index: number,
fixed: string | boolean,
store: any,
realColumns?: TableColumnCtx<T>[]
) => {
let start = 0
let after = index
if (realColumns) {
// handle group
for (let i = 0; i < index; i++) {
start += realColumns[i].colSpan
}
after = start + realColumns[index].colSpan - 1
} else {
start = index
}
let fixedLayout
const columns = store.states.columns
switch (fixed) {
case 'left':
if (after < store.states.fixedLeafColumnsLength.value) {
fixedLayout = 'left'
}
break
case 'right':
if (
start >=
columns.value.length - store.states.rightFixedLeafColumnsLength.value
) {
fixedLayout = 'right'
}
break
default:
if (after < store.states.fixedLeafColumnsLength.value) {
fixedLayout = 'left'
} else if (
start >=
columns.value.length - store.states.rightFixedLeafColumnsLength.value
) {
fixedLayout = 'right'
}
}
return fixedLayout
? {
direction: fixedLayout,
start,
after,
}
: {}
}
export const getFixedColumnsClass = <T>(
index: number,
fixed: string | boolean,
store: any,
realColumns?: TableColumnCtx<T>[]
) => {
const classes: string[] = []
const { direction, start } = isFixedColumn(index, fixed, store, realColumns)
if (direction) {
const isLeft = direction === 'left'
classes.push(`el-table-fixed-column--${direction}`)
if (isLeft && start === store.states.fixedLeafColumnsLength.value - 1) {
classes.push('is-last-column')
} else if (
!isLeft &&
start ===
store.states.columns.value.length -
store.states.rightFixedLeafColumnsLength.value
) {
classes.push('is-first-column')
}
}
return classes
}
function getOffset<T>(offset: number, column: TableColumnCtx<T>) {
return (
offset +
(Number.isNaN(column.realWidth) ? Number(column.width) : column.realWidth)
)
}
export const getFixedColumnOffset = <T>(
index: number,
fixed: string | boolean,
store: any,
realColumns?: TableColumnCtx<T>[]
) => {
const { direction, start = 0 } = isFixedColumn(
index,
fixed,
store,
realColumns
)
if (!direction) {
return
}
const styles: any = {}
const isLeft = direction === 'left'
const columns = store.states.columns.value
if (isLeft) {
styles.left = columns.slice(0, index).reduce(getOffset, 0)
} else {
styles.right = columns
.slice(start + 1)
.reverse()
.reduce(getOffset, 0)
}
return styles
}
export function getCellStyle<T>(
column: TableColumnCtx<T>,
cellIndex: number,
hasGutter: boolean,
gutterWidth: number,
store: any
) {
const fixedStyle = getFixedColumnOffset(cellIndex, column.fixed, store)
ensureRightFixedStyle(fixedStyle, hasGutter)
ensurePosition(fixedStyle, 'left')
ensurePosition(fixedStyle, 'right')
return fixedStyle
}
export const ensureRightFixedStyle = (style, hasGutter: boolean) => {
if (hasGutter && style && !Number.isNaN(style.right)) {
style.right += scrollbarWidth()
}
}
export const ensurePosition = (style, key: string) => {
if (!style) return
if (!Number.isNaN(style[key])) {
style[key] = `${style[key]}px`
}
}

View File

@@ -19,6 +19,19 @@
font-size: 14px;
color: var(--el-table-text-color);
@include e(inner-wrapper) {
position: relative;
// 表格底部伪 border,总是有的
&::before {
@include extend-rule(border-pseudo);
left: 0;
bottom: 0;
width: 100%;
height: 1px;
z-index: 3;
}
}
// when data is empty
@include e(empty-block) {
min-height: 60px;
@@ -102,7 +115,7 @@
@include m(scrollable-y) {
.#{$namespace}-table__body-wrapper {
overflow-y: auto;
overflow-y: scroll;
}
}
@@ -125,7 +138,7 @@
vertical-align: middle;
position: relative;
text-align: left;
z-index: 1;
@include when(center) {
text-align: center;
}
@@ -193,7 +206,6 @@
}
th.#{$namespace}-table__cell {
overflow: hidden;
user-select: none;
background-color: var(--el-table-header-bg-color);
@@ -233,7 +245,19 @@
// 拥有多级表头
@include m((group, border)) {
border: var(--el-table-border);
@include e((inner-wrapper, footer-wrapper)) {
tr:first-child {
td,
th {
&:first-child {
border-left: var(--el-table-border);
}
}
}
}
@include e((footer-wrapper)) {
border-top: var(--el-table-border);
}
@include share-rule(border-pseudo) {
content: '';
@@ -241,30 +265,56 @@
background-color: var(--el-table-border-color);
z-index: 1;
}
// 表格右部伪 border
&::after {
@include extend-rule(border-pseudo);
top: 0;
right: 0;
width: 1px;
height: 100%;
}
}
// 表格底部伪 border,总是有的
&::before {
@include extend-rule(border-pseudo);
left: 0;
bottom: 0;
width: 100%;
height: 1px;
}
// table--border
@include m(border) {
border-right: none;
border-bottom: none;
@include e(inner-wrapper) {
&::after {
@include extend-rule(border-pseudo);
left: 0;
top: 0;
width: 100%;
height: 1px;
z-index: 3;
}
}
&::before {
@include extend-rule(border-pseudo);
top: -1px;
left: 0;
width: 1px;
height: 100%;
z-index: 3;
}
&::after {
@include extend-rule(border-pseudo);
top: -1px;
right: 0;
width: 1px;
height: 100%;
z-index: 3;
}
@include e(inner-wrapper) {
border-right: none;
border-bottom: none;
}
@include e(footer-wrapper) {
position: relative;
}
@include e(footer-wrapper) {
margin-top: -2px;
td.#{$namespace}-table__cell {
&.last {
border-right: none;
}
}
}
// fix: #1013
// &.#{$namespace}-loading-parent--relative {
@@ -273,6 +323,9 @@
.#{$namespace}-table__cell {
border-right: var(--el-table-border);
&:first-child .cell {
padding-left: 10px;
}
}
th.#{$namespace}-table__cell.gutter:last-of-type {
@@ -289,83 +342,50 @@
visibility: hidden;
}
@include e((fixed, fixed-right)) {
position: absolute;
top: 0;
left: 0;
overflow-x: hidden;
overflow-y: hidden;
box-shadow: var(--el-table-fixed-box-shadow);
&::before {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background-color: var(--el-border-color-lighter);
z-index: 4;
}
}
@include e(fixed-right-patch) {
position: absolute;
top: -1px;
right: 0;
background-color: var(--el-table-header-bg-color);
border-bottom: var(--el-table-border);
}
@include e(fixed-right) {
top: 0;
left: auto;
right: 0;
.#{$namespace}-table__fixed-header-wrapper,
.#{$namespace}-table__fixed-body-wrapper,
.#{$namespace}-table__fixed-footer-wrapper {
left: auto;
right: 0;
}
}
@include e(fixed-header-wrapper) {
position: absolute;
left: 0;
top: 0;
z-index: 3;
}
@include e(fixed-footer-wrapper) {
position: absolute;
left: 0;
bottom: 0;
z-index: 3;
& tbody td.#{$namespace}-table__cell {
border-top: var(--el-table-border);
background-color: var(--el-table-row-hover-bg-color);
color: var(--el-table-text-color);
}
}
@include e(fixed-body-wrapper) {
position: absolute;
left: 0;
top: 37px;
overflow: hidden;
z-index: 3;
}
@include e((header-wrapper, body-wrapper, footer-wrapper)) {
width: 100%;
}
@include e(footer-wrapper) {
margin-top: -1px;
td.#{$namespace}-table__cell {
border-top: var(--el-table-border);
tr {
td,
th {
&.#{$namespace}-table-fixed-column--left,
&.#{$namespace}-table-fixed-column--right {
position: sticky !important;
z-index: 2;
background: #fff;
&.is-last-column,
&.is-first-column {
&::before {
content: '';
position: absolute;
top: 0px;
width: 10px;
bottom: -1px;
overflow-x: hidden;
overflow-y: hidden;
box-shadow: inset -10px 0 10px -10px rgb(0 0 0 / 15%);
touch-action: none;
pointer-events: none;
}
}
&.is-first-column {
&::before {
left: -10px;
}
}
&.is-last-column {
&::before {
right: -10px;
box-shadow: inset 10px 0 10px -10px rgb(0 0 0 / 15%);
}
}
}
&.#{$namespace}-table__fixed-right-patch {
position: sticky !important;
z-index: 2;
background: #fff;
right: 0;
}
}
}
}
@@ -383,42 +403,55 @@
}
}
@include when(scrolling-left) {
.#{$namespace}-table-fixed-column--left.is-last-column {
&::before {
box-shadow: none;
}
}
&.#{$namespace}-table--border {
.#{$namespace}-table-fixed-column--left {
&.is-last-column {
&.#{$namespace}-table__cell {
border-right: var(--el-table-border);
}
}
}
}
}
@include when(scrolling-right) {
.#{$namespace}-table-fixed-column--right.is-first-column {
&::before {
box-shadow: none;
}
}
.#{$namespace}-table-fixed-column--left.is-last-column.#{$namespace}-table__cell {
border-right: none;
}
}
@include when(scrolling-middle) {
.#{$namespace}-table-fixed-column--left.is-last-column.#{$namespace}-table__cell {
border-right: none;
}
}
@include when(scrolling-none) {
.#{$namespace}-table-fixed-column--left,
.#{$namespace}-table-fixed-column--right {
&.is-first-column,
&.is-last-column {
&::before {
box-shadow: none;
}
}
}
}
@include e(body-wrapper) {
overflow: hidden;
position: relative;
@include when(scrolling-none) {
~ .#{$namespace}-table__fixed,
~ .#{$namespace}-table__fixed-right {
box-shadow: none;
}
}
@include when(scrolling-left) {
~ .#{$namespace}-table__fixed {
box-shadow: none;
}
}
@include when(scrolling-right) {
~ .#{$namespace}-table__fixed-right {
box-shadow: none;
}
}
.#{$namespace}-table--border {
@include when(scrolling-right) {
~ .#{$namespace}-table__fixed-right {
border-left: var(--el-table-border);
}
}
@include when(scrolling-left) {
~ .#{$namespace}-table__fixed {
border-right: var(--el-table-border);
}
}
}
}
.caret-wrapper {
@@ -519,6 +552,33 @@
}
}
@include e(border-left-patch) {
top: 0;
left: 0;
width: 1px;
height: 100%;
z-index: 3;
position: absolute;
background-color: var(--el-table-border-color);
}
@include e(border-bottom-patch) {
left: 0;
height: 1px;
z-index: 3;
position: absolute;
background-color: var(--el-table-border-color);
}
@include e(border-right-patch) {
top: 0;
height: 100%;
width: 1px;
z-index: 3;
position: absolute;
background-color: var(--el-table-border-color);
}
@include m(enable-row-transition) {
.#{$namespace}-table__body td.#{$namespace}-table__cell {
transition: background-color 0.25s ease;
@@ -531,14 +591,6 @@
}
}
@include m(fluid-height) {
.#{$namespace}-table__fixed,
.#{$namespace}-table__fixed-right {
bottom: 0;
overflow: hidden;
}
}
[class*='#{$namespace}-table__row--level'] {
.#{$namespace}-table__expand-icon {
display: inline-block;