vue-virtualized-table-booway
Version:
The second version of implementation of `vue-virtual-table` component, it was inspired from [rc-table](https://github.com/react-component/table) and [ant-table](https://ant.design/components/table), API design is 60%+ consistent. Or you could think I tran
581 lines (510 loc) • 15 kB
JSX
/* eslint-disable no-unused-vars */
import { TableProps, TableComponents } from './interface'
import * as Expansion from './mixins/withExpansion'
import * as Selection from './mixins/withSelection'
import * as Virtualization from './mixins/withVirtualization'
import { useRaf } from './utils/useRaf'
import { getRowKey } from './utils/rowKey'
import { debounce } from './utils/debounce'
import { isNumber, isFunction, isValidArray } from './utils/type'
import getScrollBarSize from './utils/dom/getScrollBarSize'
import {
flatColumns,
getColumnsKey,
getStickyOffset,
getCellFixedInfo
} from './utils/column'
import ColGroup from './ColGroup'
import Body from './TableBody/index'
import Header from './TableHeader/index'
import FixedHeader from './TableHeader/FixedHeader'
import ResizeObserver from 'vue-size-observer'
import { forceScroll } from './utils/dom/scroll'
import './table.css'
export default {
name: 'VirtualTable',
mixins: [Selection, Expansion, Virtualization],
inheritAttrs: false,
props: TableProps,
provide() {
return {
// Table props
prefixCls: this.prefixCls,
direction: this.direction,
components: this.components,
expandable: this.expandable,
rowHeight: this.rowHeight,
rowClassName: this.rowClassName,
rowSelection: this.rowSelection,
// Computed properties
getRowKey: this.getRowKey,
fixHeader: this.fixHeader,
fixColumn: this.hasFixColumn,
horizonScroll: this.horizonScroll,
/* from `./mixins/withSelection.js` */
// isSelectionMode: this.isSelectionMode,
/* from `./mixins/withExpansion.js` */
// isExpansionMode: this.isExpansionMode,
// flattenColumns: this.flattenColumns,
// Internal state
// columnsKey: this.columnsKey,
scrollbarSize: getScrollBarSize(),
// Callback method
onColumnResize: this.onColumnResize,
scopedSlots: this.getScopedSlots,
// Operations method
store: {
isRowExpanded: this.isRowExpanded,
toggleRowExpansion: this.toggleRowExpansion,
toggleExpandAll: this.toggleExpandAll,
toggleExpandDepth: this.toggleExpandDepth,
isRowSelected: this.isRowSelected,
toggleRowSelection: this.toggleRowSelection,
toggleColumnSelection: this.toggleColumnSelection
}
}
},
data() {
// NOTE: 必须返回空对象,用以给其他组件 Mixin
// 否则会报错 Vue mixin mergeOptions 会报错
return {
pingedLeft: false,
pingedRight: false,
componentWidth: 0,
colWidths: {
/* [columnKey as string]: number */
}
}
},
// 计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值
computed: {
hasData() {
return isValidArray(this.dataSource)
},
currentData() {
// virtualized & virtualizedData FROM `mixins/withVirtualization`
return this.virtualized ? this.virtualizedData : this.dataSource
},
fixHeader() {
return !!(this.hasData && this.scroll && this.scroll.y)
},
horizonScroll() {
return !!(this.scroll && this.scroll.x)
},
flattenColumns() {
return flatColumns(this.columns, this.direction)
},
stickyOffsets() {
return getStickyOffset(
this.columnsWidths,
this.flattenColumns.length,
this.direction
)
},
fixedInfoList() {
return this.flattenColumns.map((_, colIndex) =>
getCellFixedInfo(
colIndex,
colIndex,
this.flattenColumns,
this.stickyOffsets,
this.direction
)
)
},
columnsKey() {
return getColumnsKey(this.flattenColumns)
},
columnsWidths() {
return this.columnsKey.map((key) => this.colWidths[key])
},
hasFixColumn() {
return this.flattenColumns.some((column) => column.fixed)
},
hasFixLeftColumn() {
return !!(this.flattenColumns[0] && this.flattenColumns[0].fixed)
},
hasFixRightColumn() {
const lastIndex = this.flattenColumns.length - 1
return (
this.flattenColumns[lastIndex] &&
this.flattenColumns[lastIndex].fixed === 'right'
)
},
finalTableLayout() {
const { tableLayout, fixHeader, hasFixColumn, flattenColumns } = this
if (tableLayout) {
return tableLayout
}
if (
fixHeader ||
hasFixColumn ||
flattenColumns.some(({ ellipsis }) => ellipsis)
) {
return 'fixed'
}
return 'auto'
}
},
watch: {
components(value) {
this.tableComponents = Object.assign({}, TableComponents, value)
}
},
created() {
// this.scrollbarSize = getScrollBarSize()
this.tableComponents = Object.assign({}, TableComponents, this.components)
// It is just a flag to cache the value
// and used for do not update view instantly
this.columnsWidthCache = []
this.debouncedUpdateColumn = debounce(this.updateColumnResize, 16)
},
mounted() {
this.$ready = true
},
beforeDestroy() {
this.rowKeyCache = null
this.tableComponents = null
this.columnsWidthCache = null
},
methods: {
toggleColumnSelection(record, event) {
this.$emit(
'dbl-click',
record,
event
)
},
getScopedSlots() {
return this.$scopedSlots
},
/**
* getRowKey
* @param {RowModel} row
* @param {number} index
* @returns {string|number}
*/
getRowKey(row, index) {
/**
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
* https://academia.stackexchange.com/questions/149620/sharing-with-research-team-about-being-affected-by-a-natural-disaster
*/
const cache = this.rowKeyCache || (this.rowKeyCache = new WeakMap())
if (cache.has(row)) {
return cache.get(row)
} else {
const key = getRowKey(row, index, this.rowKey)
cache.set(row, key)
return key
}
},
renderBlock(block, name) {
const content =
typeof block === 'function' ? block(this.dataSource) : block
// const height = this.scroll.y
let style = {}
/* if (name === 'placeholder' && height) {
style = {
height: height + 'px'
}
} */
return content ? (
<div key={name} class={`${this.prefixCls}-${name}`} style={style}>
{content}
</div>
) : null
},
renderEmptyText() {
const {
locale: { emptyText },
dataSource
} = this
if (dataSource.length) {
return null
}
return this.renderBlock(
this.$slots.empty ? this.$slots.empty : emptyText,
'placeholder'
)
},
setScrollTarget(target) {
const timerId = setTimeout(() => {
this.scrollTarget = target
const clearRafId = useRaf(() => {
this.scrollTarget = null
clearTimeout(timerId)
clearRafId()
})
})
},
onFullTableResize({ width }) {
this.triggerOnScroll()
const { fullTableRef } = this.$refs
this.componentWidth = fullTableRef ? fullTableRef.offsetWidth : width
this.updateVirtualizedData(true)
},
triggerOnScroll() {
const { scrollBodyRef } = this.$refs
if (scrollBodyRef) {
this.onScroll({ currentTarget: scrollBodyRef })
}
},
onScroll({ currentTarget, scrollLeft, scrollTop }) {
const mergedScrollLeft = isNumber(scrollLeft)
? scrollLeft
: currentTarget.scrollLeft
const mergedScrollTop = isNumber(scrollTop)
? scrollTop
: currentTarget.scrollTop
const compareTarget = currentTarget || undefined
const { scrollHeaderRef, scrollBodyRef } = this.$refs
if (compareTarget === scrollBodyRef && isNumber(mergedScrollTop)) {
// from `./mixins/withVirtualization.js`
this.updateVirtualizedData(mergedScrollTop)
}
if (!this.scrollTarget || this.scrollTarget === compareTarget) {
this.setScrollTarget(compareTarget)
forceScroll(mergedScrollLeft, scrollHeaderRef)
forceScroll(mergedScrollLeft, scrollBodyRef)
}
if (currentTarget) {
const { scrollWidth, clientWidth } = currentTarget
this.pingedLeft = mergedScrollLeft > 0
this.pingedRight = mergedScrollLeft < scrollWidth - clientWidth
}
},
/**
* onColumnResize
* @param {string} columnKey
* @param {number} width
*/
onColumnResize(columnKey, width) {
this.columnsWidthCache = {
...this.columnsWidthCache,
[columnKey]: width
}
this.debouncedUpdateColumn()
},
updateColumnResize() {
const cancelRef = useRaf(() => {
this.colWidths = {
...this.columnsWidthCache
}
const cancelRefAgain = useRaf(() => {
cancelRef()
cancelRefAgain()
})
isFunction(this.debouncedUpdateColumn.cancel) &&
this.debouncedUpdateColumn.cancel()
})
}
},
render() {
const {
// Table Props
prefixCls,
direction,
bordered,
size,
scroll,
columns,
components,
showHeader,
expandable = {},
// Internal data
colWidths,
pingedLeft,
pingedRight,
componentWidth,
// Computed Properties
hasData,
fixHeader,
columnsKey,
currentData,
virtualized /* `mixins/withVirtualization` */,
hasFixColumn,
columnsWidths,
horizonScroll,
stickyOffsets,
fixedInfoList,
flattenColumns,
// isSelectionMode /* from `./mixins/withSelection.js` */,
// isExpansionMode /* from `./mixins/withExpansion.js` */,
expandedRowKeys /* from `./mixins/withExpansion.js` */,
finalTableLayout: tableLayout,
hasFixLeftColumn,
hasFixRightColumn,
// Panel block
title,
footer
} = this
// this.$scopedSlots["商品名称"]({ text: "传过去的值" })
const { table: TableComponent } = components
const { rowExpandable, childrenColumnName = 'children' } = expandable
const fixColumn = horizonScroll && hasFixColumn
const hasFixLeft = hasFixLeftColumn
const hasFixRight = hasFixRightColumn
let scrollXStyle
let scrollYStyle
let scrollTableStyle
if (fixHeader) {
scrollYStyle = {
overflow: 'scroll',
maxHeight: scroll.y + 'px'
}
}
if (horizonScroll) {
scrollXStyle = { overflowX: 'scroll' }
// When no vertical scrollbar, should hide it
if (!fixHeader) {
scrollYStyle = { overflowY: 'hidden' }
}
scrollTableStyle = {
width:
scroll.x === true
? 'auto'
: isNumber(scroll.x)
? scroll.x + 'px'
: scroll.x,
minWidth: '100%'
}
}
let groupTableNode
const emptyNode = hasData ? null : this.renderEmptyText()
const headerProps = {
props: {
// colWidths,
columCount: flattenColumns.length,
stickyOffsets,
columns,
flattenColumns
}
}
const bodyTable = (
<Body
data={currentData}
columnsKey={columnsKey}
stickyOffsets={stickyOffsets}
fixedInfoList={fixedInfoList}
flattenColumns={flattenColumns}
expandedKeys={expandedRowKeys}
rowExpandable={rowExpandable}
componentWidth={componentWidth}
measureColumnWidth={fixHeader || horizonScroll}
childrenColumnName={childrenColumnName}
emptyNode={emptyNode}
// onRow={onRow}
/>
)
const bodyColGroup = (
<ColGroup
colWidths={flattenColumns.map(({ width }) => width)}
columns={flattenColumns}
/>
)
if (fixHeader) {
const tableNode = (
<TableComponent
style={{
...scrollTableStyle,
tableLayout
}}
>
{bodyColGroup}
{bodyTable}
</TableComponent>
)
groupTableNode = [
/* Header Table */
showHeader !== false && (
<div
style={{
overflow: 'hidden'
}}
onScroll={this.onScroll}
ref="scrollHeaderRef"
class={`${prefixCls}-header`}
>
{
<FixedHeader
{...headerProps}
direction={direction}
colWidths={columnsWidths}
/>
}
</div>
),
/* Body Table */
<div
style={{
...scrollXStyle,
...scrollYStyle
}}
onScroll={this.onScroll}
ref="scrollBodyRef"
class={`${prefixCls}-body`}
>
{virtualized ? this.renderVirtualizedWrapper(tableNode) : tableNode}
</div>
]
} else {
groupTableNode = (
<div
style={{
...scrollXStyle,
...scrollYStyle
}}
class={`${prefixCls}-content`}
onScroll={this.onScroll}
ref="scrollBodyRef"
>
<TableComponent style={{ ...scrollTableStyle, tableLayout }}>
{bodyColGroup}
{showHeader !== false && (
<Header
colWidths={colWidths}
columCount={flattenColumns.length}
stickyOffsets={stickyOffsets}
{...headerProps}
/>
)}
{bodyTable}
</TableComponent>
</div>
)
}
let fullTable = (
<div
class={[
prefixCls,
size && size !== 'default' && `${prefixCls}-${size}`,
tableLayout === 'fixed' && `${prefixCls}-layout-fixed`,
!hasData && `${prefixCls}-empty`,
bordered && `${prefixCls}-bordered`,
pingedLeft && `${prefixCls}-ping-left`,
pingedRight && `${prefixCls}-ping-right`,
fixHeader && `${prefixCls}-fixed-header`,
hasFixLeft && `${prefixCls}-has-fix-left`,
hasFixRight && `${prefixCls}-has-fix-right`,
virtualized && `${prefixCls}-virtualized`,
/** No used but for compatible */
fixColumn && `${prefixCls}-fixed-column`,
horizonScroll && `${prefixCls}-scroll-horizontal`
]}
ref="fullTableRef"
>
{title && this.renderBlock(title, 'title')}
<div class={`${prefixCls}-container`}>{groupTableNode}</div>
{footer && this.renderBlock(footer, 'footer')}
</div>
)
if (horizonScroll) {
fullTable = (
<ResizeObserver onResize={this.onFullTableResize}>
{fullTable}
</ResizeObserver>
)
}
return fullTable
}
}