UNPKG

ans-ui

Version:

A vue-based UI components library for analysys

718 lines (646 loc) 23.5 kB
import Vue from 'vue' import * as util from './util' import { debounce, throttle } from 'throttle-debounce' export default class TableStore { constructor (table) { this.table = table this.scheduleMappingSortedData = debounce(10, () => { table.layout.updateColumnsWidth() this.mappingSortedData() }) this.scheduleUpdateColumns = debounce(10, (flag) => this.updateColumns(flag)) this.throttleCalculateIndexes = throttle(50, (top) => this.calculateIndexes(top)) this.throttleCalculateColumnIndexes = throttle(20, (left) => this.calculateColumnIndexes(left)) this.states = { // 原始数据 _data: null, _sortedData: null, _mappedData: null, _slicedData: null, // 当前展示数据 data: null, rows: [], renderedRows: [], _columns: [], columns: [], renderedColumns: [], notFixedColumns: [], notFixedLeafColumns: [], leafColumns: [], leafColumnMap: {}, colColumns: [], colColumnIds: [], renderedColColumns: [], fixedLeftColumns: [], fixedLeftLeafColumns: [], fixedRightColumns: [], fixedRightLeafColumns: [], multiLayer: false, hasFixedTable: false, hoverRowIndex: null, selection: [], isAllSelected: false, isIndeterminate: false, expandable: false, expandRows: [], treeType: false, childrenProp: table.childrenProp, parentRows: [], unfoldedRows: [], sortingColumn: null, sortProp: null, sortOrder: '', defaultSortOrders: util.ORDERS, dragging: false, draggingColumn: null, dragModel: {}, rowLimit: null, startRowIndex: 0, endRowIndex: null, startColumnIndex: 0, transformXAmount: 0, transformYAmount: 0, contentHeight: null, firstRowSpan: 1, renderedRowCount: 0, renderedColumnCount: 0, destroying: false } if (table.defaultSortOrders) { if (util.checkSortOrders(table.defaultSortOrders)) { this.states.defaultSortOrders = table.defaultSortOrders } } this.mutations = { insertColumn (states, column, index, parent) { if (!column.sortOrders || !util.checkSortOrders(column.sortOrders)) { column.sortOrders = states.defaultSortOrders } let array = states._columns if (parent) { array = parent.children if (!array) array = parent.children = [] } if (typeof index !== 'undefined') { array.splice(index, 0, column) } else { array.push(column) } if (column.type === 'expand') { states.expandable = true if (this.table.hideExpandIcon) { array.pop() } } if (this.table.$ready) { this.scheduleUpdateColumns(true) this.scheduleLayout() } }, removeColumn (states, column, parent) { let array = states._columns if (parent) { array = parent.children if (!array) array = parent.children = [] } if (array) { array.splice(array.indexOf(column), 1) } if (this.table.$ready) { this.scheduleUpdateColumns(true) this.scheduleLayout() } }, rowSelectionChanged (states, row) { const changed = util.toggleRowSelection(states, row) if (changed) { const selection = states.selection ? states.selection.slice() : [] const table = this.table table.$emit('on-selection-change', selection) table.$emit('on-select', selection, row) } this.updateAllSelected() }, allSelectionChanged (states) { states.isAllSelected = !states.isAllSelected states.isIndeterminate = false if (states.isAllSelected) { states.selection = states._sortedData.slice() } else { states.selection = [] } table.$emit('on-selection-change', states.selection) table.$emit('on-select-all', states.selection) }, rowExpansionChanged (states, row) { this.toggleRowExpansion(row) }, rowUnfoldingChanged (states, row) { this.toggleRowUnfolding(row) }, sortConditionChanged (states, column, asc) { let order if (typeof asc === 'boolean') { order = column.order = asc ? util.ASCENDING_ORDER : util.DESCENDING_ORDER } else { order = column.order = util.getNextOrder(column) } const changed = util.toggleSortCondition(states, column, order) if (changed) { if (column.sortable !== 'custom') { states._sortedData = Object.freeze(util.sortData(states._data, states)) this.mappingSortedData() Vue.nextTick(() => this.table.doLayout()) } this.table.$emit('on-sort-change', column, states.sortProp, states.sortOrder) } }, setHoverRowIndex (states, rowIndex) { states.hoverRowIndex = rowIndex }, setDraggingState (states, dragging) { states.dragging = dragging }, setDraggingColumn (states, draggingColumn) { states.draggingColumn = draggingColumn }, setDragModel (states, dragModel) { states.dragModel = dragModel } } } scheduleLayout (updateColumns) { if (updateColumns) { this.updateColumns() } this.table.debouncedUpdateLayout() } setData (data) { const states = this.states const dataInstanceChanged = states._data !== data states._data = this.table.frozenData ? Object.freeze(data) : data if (states.childrenProp) { util.processTreeData(states, this.table) } states._sortedData = Object.freeze(util.sortData(states._data, states)) const { defaultExpandAll, rowKey, reserveStates, defaultUnfoldChildren } = this.table if (defaultExpandAll) { states.expandRows = (states._sortedData || []).slice() } else { if (reserveStates) { states.expandRows = util.reserve(states.expandRows, states._sortedData, rowKey) } else { if (dataInstanceChanged) { this.clearExpandRows() } else { this.cleanExpandRows() } } } if (reserveStates) { states.selection = util.reserve(states.selection, states._sortedData, rowKey) this.updateAllSelected() } else { if (dataInstanceChanged) { this.clearSelection() } else { this.cleanSelection() this.updateAllSelected() } } if (defaultUnfoldChildren) { states.parentRows.forEach((r) => { util.toggleRowUnfolding(states, r, true) }) } this.mappingSortedData() } toggleAllSelection () { const states = this.states states.isAllSelected = !states.isAllSelected states.isIndeterminate = false if (states.isAllSelected) { states.selection = states._sortedData.slice() } else { states.selection = [] } this.table.$emit('on-selection-change', states.selection) } clearSelection () { const states = this.states states.isAllSelected = false states.isIndeterminate = false const oldSelection = states.selection if (states.selection.length) { states.selection = [] } if (oldSelection.length > 0) { this.table.$emit('on-selection-change', []) } } cleanSelection () { const states = this.states const rowKey = this.table.rowKey const { changed, list: selection } = util.clean(states.selection, states._sortedData, rowKey) states.selection = selection if (changed) { this.table.$emit('on-selection-change', selection ? selection.slice() : []) } } toggleRowSelection (row, selected) { const changed = util.toggleRowSelection(this.states, row, selected) const selection = this.states.selection if (changed) { this.table.$emit('on-selection-change', selection ? selection.slice() : []) } this.updateAllSelected() } updateAllSelected () { const { selection, _sortedData } = this.states // 简易判断 this.states.isAllSelected = selection && _sortedData && selection.length === _sortedData.length this.states.isIndeterminate = !this.states.isAllSelected && selection && selection.length } clearExpandRows () { const states = this.states const oldexpandRows = states.expandRows if (states.expandRows.length) { states.expandRows = [] } if (oldexpandRows.length > 0) { this.table.$emit('on-expand-change', []) } } cleanExpandRows () { const states = this.states const rowKey = this.table.rowKey const { changed, list: expandRows } = util.clean(states.expandRows, states._sortedData, rowKey) states.expandRows = expandRows if (changed) { this.table.$emit('on-expand-change', expandRows ? expandRows.slice() : []) } } toggleRowExpansion (row, expanded) { const changed = util.toggleRowExpansion(this.states, row, expanded) if (changed) { this.table.$emit('on-expand-change', row, this.states.expandRows) Vue.nextTick(() => this.scheduleLayout()) } } toggleRowUnfolding (row, unfolded) { const changed = util.toggleRowUnfolding(this.states, row, unfolded) if (changed) { this.mappingSortedData() this.table.$emit('on-unfolded-change', row, this.states.unfoldedRows) Vue.nextTick(() => this.table.doLayout()) } } setExpandRowKeys (rowKeys) { const expandRows = [] const rowKey = this.table.rowKey if (!rowKey) throw new Error('Table Store: Prop row-key should not be empty.') const keysMap = util.getKeysMap(this.states._sortedData || [], rowKey) rowKeys.forEach((key) => { const info = keysMap[key] if (info) { expandRows.push(info.row) } }) this.states.expandRows = expandRows Vue.nextTick(() => this.table.doLayout()) } clearSort () { if (!this.states.sortingColumn) return const changed = util.toggleSortCondition(this.states, this.states.sortingColumn, '') if (changed) { this.table.$emit('on-sort-change', null, null, '') Vue.nextTick(() => this.table.doLayout()) } } sort (prop, order) { const states = this.states const column = states.leafColumns.find((c) => c.prop === prop) if (!column) { throw new Error(`Table Store: Can't find target column with prop '${prop}'!`) } if (typeof order === 'string' && !util.ORDERS.includes(order)) { throw new Error(`Table Store: Sort order must be '${util.ASCENDING_ORDER}' or '${util.DESCENDING_ORDER}'!`) } if (typeof order === 'undefined') { order = column.order = util.getNextOrder(column) } const changed = util.toggleSortCondition(states, column, order) if (changed) { if (column.sortable !== 'custom') { states._sortedData = Object.freeze(util.sortData(states._data, states)) this.mappingSortedData() Vue.nextTick(() => this.table.doLayout()) } this.table.$emit('on-sort-change', column, states.sortProp, states.sortOrder) } } updateColumns (debouncedMapping) { if (this.states.destroying) return const states = this.states const _columns = states._columns || [] states.fixedLeftColumns = Object.freeze(_columns.filter(column => column.fixed === true || column.fixed === 'left')) states.fixedRightColumns = Object.freeze(_columns.filter(column => column.fixed === 'right')) if (states.fixedLeftColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) { _columns[0].fixed = true const temp = states.fixedLeftColumns.slice() temp.unshift(_columns[0]) states.fixedLeftColumns = Object.freeze(temp) } const notFixedColumns = _columns.filter(column => !column.fixed) states.notFixedColumns = Object.freeze(notFixedColumns) states.notFixedLeafColumns = Object.freeze(util.doFlattenColumns(notFixedColumns)) states.columns = Object.freeze([].concat(states.fixedLeftColumns).concat(notFixedColumns).concat(states.fixedRightColumns)) for (let i = 0; i < states.columns.length; i++) { const c = states.columns[i] c._index = i } states.rows = Object.freeze(util.convertToRows(states.columns)) for (const r of states.rows) { for (let i = 0; i < r.length; i++) { const c = r[i] c._indexInRow = i } } states.multiLayer = states.rows.length > 1 states.leafColumns = Object.freeze(util.doFlattenColumns(states.columns)) const columnsMap = {} states.leafColumns.forEach((column) => { columnsMap[column.id] = column }) states.leafColumnMap = columnsMap states.colColumns = Object.freeze(states.multiLayer ? states.leafColumns : states.columns) states.fixedLeftLeafColumns = Object.freeze(util.doFlattenColumns(states.fixedLeftColumns)) states.fixedRightLeafColumns = Object.freeze(util.doFlattenColumns(states.fixedRightColumns)) states.hasFixedTable = states.fixedLeftColumns.length > 0 || states.fixedRightColumns.length > 0 if (debouncedMapping) { this.scheduleMappingSortedData() } else { this.mappingSortedData() } } commit (name, ...args) { const mutations = this.mutations if (mutations[name]) { mutations[name].apply(this, [this.states].concat(args)) } else { throw new Error(`Table Store: Action not found: ${name}`) } } updateRenderData () { const { colColumns, colColumnIds } = this.states const changed = colColumns.some(c => !colColumnIds.includes(c.id)) if (changed) { this.mappingSortedData() } else { this.sliceData() } } mappingSortedData () { const { rowHeight, rowKey, stripe, cellSpanMethod, $el } = this.table const states = this.states if (states.destroying || ($el && $el.clientHeight === 0)) return // mapping all data if (states.colColumns.length) { states.colColumnIds = Object.freeze(states.colColumns.map(c => c.id)) states.contentHeight = states._sortedData.length * rowHeight // data + columns => cells states._mappedData = Object.freeze(states._sortedData.map((d, i) => { const cells = [] states.colColumns.forEach((c, j) => { const model = { columnIndex: j, column: c, content: util.getCellContent(d, c, i, j), spanModel: util.getSpanModel(d, c, i, j, cellSpanMethod) } if (model.spanModel.rowspan > 1 && states.firstRowSpan === 1) { states.firstRowSpan = model.spanModel.rowspan } cells.push(model) }) return { item: d, rowIndex: i, cells, key: util.getRowKey(i, d, rowKey), classes: util.getRowClasses(i, stripe) } })) this.sliceData() } } sliceData () { const { virtualScroll, rowHeight, layout, $el, maxTreeRow } = this.table const states = this.states if (!states._mappedData || ($el && $el.clientHeight === 0)) return if (virtualScroll) { if (layout.scrollY) { let rowCount if (!layout.bodyHeight) { // init render rowCount = 10 } else { rowCount = Math.ceil(layout.bodyHeight / rowHeight) } states.rowLimit = states.firstRowSpan !== 1 ? (Math.ceil(rowCount / states.firstRowSpan) + 1) * states.firstRowSpan : rowCount + 6 this.calculateIndexes() return } else if (states.treeType) { states._slicedData = Object.freeze(states._mappedData.slice(states.startRowIndex, states.renderedRowCount + maxTreeRow)) this.calculateColumnIndexes() return } } states._slicedData = Object.freeze(states._mappedData.slice()) this.calculateColumnIndexes() } calculateIndexes (top) { const states = this.states const { rowLimit, _mappedData, firstRowSpan } = states if (!rowLimit || !_mappedData || !_mappedData.length) return const { $refs, rowHeight, cellSpanMethod } = this.table this.commit('setHoverRowIndex', null) const currentTop = top || ($refs.scroller && $refs.scroller.currentTop) || 0 const remainCount = firstRowSpan === 1 ? 3 : firstRowSpan let startIndex = Math.floor(Math.abs(currentTop) / rowHeight) - remainCount if (startIndex < 0) { startIndex = 0 } // fix rowspan let realStartIndex = startIndex if (cellSpanMethod) { if (startIndex > states.startRowIndex) { for (let i = states.startRowIndex; i < startIndex; i++) { const currentRow = _mappedData[i] const targetCell = currentRow.cells.find(c => c.spanModel.rowspan > 1) if (targetCell && targetCell.spanModel.rowspan + i >= startIndex) { realStartIndex = i break } } } else if (startIndex < states.startRowIndex) { for (let i = states.startRowIndex; i >= 0; i--) { const currentRow = _mappedData[i] const targetCell = currentRow.cells.find(c => c.spanModel.rowspan > 1) if (targetCell && i < startIndex && targetCell.spanModel.rowspan + i >= startIndex) { realStartIndex = i break } } } } const diff = startIndex - realStartIndex let endIndex = realStartIndex + rowLimit + diff if (endIndex - realStartIndex < states.renderedRowCount) { endIndex = states.renderedRowCount + realStartIndex if (endIndex > _mappedData.length) { endIndex = _mappedData.length realStartIndex = Math.max(0, endIndex - states.renderedRowCount) } } states.renderedRowCount = endIndex - realStartIndex states.startRowIndex = realStartIndex states.endRowIndex = endIndex states.transformYAmount = realStartIndex * rowHeight this.table.updateTranslateY(states.transformYAmount) states._slicedData = Object.freeze(_mappedData.slice(realStartIndex, endIndex)) this.calculateColumnIndexes() } calculateColumnIndexes (left) { const { scrollX, tableWrapperWidth, fixedLeftWidth, fixedRightWidth } = this.table.layout const states = this.states const { notFixedColumns, notFixedLeafColumns, fixedLeftColumns, fixedRightColumns, multiLayer } = states const contentWidth = tableWrapperWidth - fixedLeftWidth - fixedRightWidth if (contentWidth < 0) return if (!states._slicedData || !states._slicedData.length) { states.data = [] states.renderedColumns = Object.freeze(states.columns) states.renderedColColumns = Object.freeze(states.colColumns) states.renderedColumnCount = states.renderedColColumns.length Vue.nextTick(() => { this.table.layout.notifyObservers('columns') this.table.debouncedCheckScrollable() }) return } // 不需要切割列 if (!scrollX || !this.table.virtualScroll) { states.transformXAmount = 0 states.renderedColumns = Object.freeze(states.columns) states.renderedColColumns = Object.freeze(states.colColumns) states.renderedRows = Object.freeze(states.rows) states.renderedColumnCount = 0 if (states._slicedData) { const columnCount = states._slicedData[0] ? states._slicedData[0].cells.length : 0 if (states.colColumns.length === columnCount) { states._slicedData.forEach(r => { r.renderedCells = r.cells.slice() }) states.data = Object.freeze(states._slicedData.slice()) this.table.$emit('on-slice') this.table.layout.updateTableWidth() Vue.nextTick(() => { this.table.layout.notifyObservers('columns') this.table.debouncedCheckScrollable() }) } } return } const currentLeft = left || (this.table.$refs.scroller && this.table.$refs.scroller.currentLeft) || 0 let _left = 0 let _width = 0 let startIndex = 0 let endIndex = 1 let findStart = false for (let i = 0; i < notFixedLeafColumns.length; i++) { const leaf = notFixedLeafColumns[i] const cWidth = leaf.currentWidth _left -= cWidth if (!findStart) { if (_left < currentLeft) { _width = currentLeft - _left // multiLayer header let parent = leaf.parent if (multiLayer && parent) { while (parent.parent) { parent = parent.parent } startIndex = notFixedColumns.indexOf(parent) const siblings = util.doFlattenColumns([parent]) for (const s of siblings) { if (s === leaf) { break } else { _width -= s.currentWidth } } } else { startIndex = Math.max(0, i - 2) } findStart = true } } else { _width += cWidth if (_width >= contentWidth) { let parent = leaf.parent if (multiLayer && parent) { while (parent.parent) { parent = parent.parent } endIndex = notFixedColumns.indexOf(parent) } else { endIndex = i + 2 } break } } } if (endIndex < startIndex || endIndex > notFixedColumns.length - 1) { endIndex = notFixedColumns.length - 1 if (states.renderedColumnCount !== 0) { startIndex = Math.max(0, endIndex - states.renderedColumnCount + 1) } } else if (endIndex - startIndex + 1 < states.renderedColumnCount) { endIndex = states.renderedColumnCount + startIndex - 1 } let transformXAmount = states.transformXAmount if (states.startColumnIndex > startIndex) { // to right const leafs = util.doFlattenColumns(notFixedColumns.slice(startIndex, states.startColumnIndex)) for (const l of leafs) { transformXAmount -= l.currentWidth } } else { // to left const leafs = util.doFlattenColumns(notFixedColumns.slice(states.startColumnIndex, startIndex)) for (const l of leafs) { transformXAmount += l.currentWidth } } states.transformXAmount = transformXAmount states.startColumnIndex = startIndex states.renderedColumns = Object.freeze([].concat(fixedLeftColumns).concat(notFixedColumns.slice(startIndex, endIndex + 1)).concat(fixedRightColumns)) states.renderedColumnCount = states.renderedColumns.length - fixedLeftColumns.length - fixedRightColumns.length states.renderedColColumns = Object.freeze(multiLayer ? util.doFlattenColumns(states.renderedColumns) : states.renderedColumns) states.renderedRows = Object.freeze(util.convertToRows(states.renderedColumns)) states._slicedData.forEach(r => { r.renderedCells = r.cells.filter(c => states.renderedColColumns.includes(c.column)) }) states.data = Object.freeze(states._slicedData.slice()) this.table.$emit('on-slice') const tableWidth = states.renderedColColumns.reduce((p, c) => p + c.currentWidth, 0) this.table.layout.updateTableWidth(tableWidth) Vue.nextTick(() => { this.table.layout.notifyObservers('columns') this.table.debouncedCheckScrollable() }) } }