UNPKG

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
/* 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 } }