From 8436753254e39b30007c37aef59ead4a8e26f6ff Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Wed, 22 May 2019 12:16:29 +0800 Subject: [PATCH 01/18] Table: refactor code --- packages/table/src/config.js | 99 ++++ packages/table/src/filter-panel.vue | 11 - packages/table/src/store/current.js | 45 ++ packages/table/src/store/expand.js | 65 +++ packages/table/src/store/index.js | 180 +++++++ packages/table/src/store/tree.js | 44 ++ packages/table/src/store/watcher.js | 371 ++++++++++++++ packages/table/src/table-body.js | 199 ++++---- packages/table/src/table-column.js | 555 ++++++++------------ packages/table/src/table-header.js | 70 ++- packages/table/src/table-store.js | 753 ---------------------------- packages/table/src/table.vue | 100 +--- packages/table/src/util.js | 132 +++++ 13 files changed, 1281 insertions(+), 1343 deletions(-) create mode 100644 packages/table/src/config.js create mode 100644 packages/table/src/store/current.js create mode 100644 packages/table/src/store/expand.js create mode 100644 packages/table/src/store/index.js create mode 100644 packages/table/src/store/tree.js create mode 100644 packages/table/src/store/watcher.js delete mode 100644 packages/table/src/table-store.js diff --git a/packages/table/src/config.js b/packages/table/src/config.js new file mode 100644 index 00000000000..9b694a7b1b7 --- /dev/null +++ b/packages/table/src/config.js @@ -0,0 +1,99 @@ +import { getPropByPath } from 'element-ui/src/utils/util'; + +export const cellStarts = { + default: { + order: '' + }, + selection: { + width: 48, + minWidth: 48, + realWidth: 48, + order: '', + className: 'el-table-column--selection' + }, + // TODO: 考虑移除 + expand: { + width: 48, + minWidth: 48, + realWidth: 48, + order: '' + }, + index: { + width: 48, + minWidth: 48, + realWidth: 48, + order: '' + } +}; + +// 这些选项不应该被覆盖 +export const cellForced = { + selection: { + renderHeader: function(h, { store }) { + return 0 && !this.isAllSelected } + nativeOn-click={ this.toggleAllSelection } + value={ this.isAllSelected } />; + }, + renderCell: function(h, { row, column, store, $index }) { + return event.stopPropagation() } + value={ store.isSelected(row) } + disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false } + on-input={ () => { store.commit('rowSelectedChanged', row); } } />; + }, + sortable: false, + resizable: false + }, + index: { + renderHeader: function(h, { column }) { + return column.label || '#'; + }, + renderCell: function(h, { $index, column }) { + let i = $index + 1; + const index = column.index; + + if (typeof index === 'number') { + i = $index + index; + } else if (typeof index === 'function') { + i = index($index); + } + + return
{ i }
; + }, + sortable: false + }, + // TODO: 考虑移除 + expand: { + renderHeader: function(h, { column }) { + return column.label || ''; + }, + renderCell: function(h, { row, store }) { + const classes = ['el-table__expand-icon']; + if (store.states.expandRows.indexOf(row) > -1) { + classes.push('el-table__expand-icon--expanded'); + } + const callback = function(e) { + e.stopPropagation(); + store.toggleRowExpansion(row); + }; + return (
+ +
); + }, + sortable: false, + resizable: false, + className: 'el-table__expand-column' + } +}; + +export function defaultRenderCell(h, { row, column, $index }) { + const property = column.property; + const value = property && getPropByPath(row, property).v; + if (column && column.formatter) { + return column.formatter(row, column, value, $index); + } + return value; +} diff --git a/packages/table/src/filter-panel.vue b/packages/table/src/filter-panel.vue index 9ff7b452471..d49f01277d9 100644 --- a/packages/table/src/filter-panel.vue +++ b/packages/table/src/filter-panel.vue @@ -72,17 +72,6 @@ } }, - customRender(h) { - return (
-
-
-
- - -
-
); - }, - methods: { isActive(filter) { return filter.value === this.filterValue; diff --git a/packages/table/src/store/current.js b/packages/table/src/store/current.js new file mode 100644 index 00000000000..bde8393c578 --- /dev/null +++ b/packages/table/src/store/current.js @@ -0,0 +1,45 @@ +import { arrayFind } from 'element-ui/src/utils/util'; +import { getRowIdentity } from '../util'; + +export default { + data() { + return { + states: { + current: null + } + }; + }, + + methods: { + setCurrentRowKey(key) { + this.assertRowKey(); + + const { states } = this; + const { data = [], rowKey } = states; + const currentRow = arrayFind(data, item => getRowIdentity(item, rowKey) === key); + states.currentRow = currentRow ? currentRow : null; + }, + + updateCurrentRow() { + const { states, table } = this; + const { rowKey } = states; + // data 为 null 时,结构时的默认值会被忽略 + const data = states.data || []; + const oldCurrentRow = states.currentRow; + + // 当 currentRow 不在 data 中时尝试更新数据 + if (data.indexOf(oldCurrentRow) === -1 && oldCurrentRow) { + let newCurrentRow = null; + if (rowKey) { + newCurrentRow = arrayFind(data, item => { + return getRowIdentity(item, rowKey) === getRowIdentity(oldCurrentRow, rowKey); + }); + } + states.currentRow = newCurrentRow; + if (newCurrentRow !== oldCurrentRow) { + table.$emit('current-change', null, oldCurrentRow); + } + } + } + } +}; diff --git a/packages/table/src/store/expand.js b/packages/table/src/store/expand.js new file mode 100644 index 00000000000..d90996e7bb7 --- /dev/null +++ b/packages/table/src/store/expand.js @@ -0,0 +1,65 @@ +import { toggleRowStatus, getKeysMap, getRowIdentity } from '../util'; + +export default { + data() { + return { + states: { + defaultExpandAll: false, + expandRows: [] + } + }; + }, + + methods: { + updateExpandRows() { + const { data = [], rowKey, defaultExpandAll, expandRows } = this.states; + if (defaultExpandAll) { + this.states.expandRows = data.slice(); + } else if (rowKey) { + // TODO:这里的代码可以优化 + const expandRowsMap = getKeysMap(expandRows, rowKey); + this.states.expandRows = data.reduce((prev, row) => { + const rowId = getRowIdentity(row, rowKey); + const rowInfo = expandRowsMap[rowId]; + if (rowInfo) { + prev.push(row); + } + return prev; + }, []); + } else { + this.states.expandRows = []; + } + }, + + toggleRowExpansion(row, expanded) { + const changed = toggleRowStatus(this.states.expandRows, row, expanded); + if (changed) { + this.table.$emit('expand-change', row, this.states.expandRows); + this.scheduleLayout(); + } + }, + + setExpandRowKeys(rowKeys) { + this.assertRowKey(); + // TODO:这里的代码可以优化 + const { data, rowKey } = this.states; + const keysMap = getKeysMap(data, rowKey); + this.states.expandRows = rowKeys.reduce((prev, cur) => { + const info = keysMap[cur]; + if (info) { + prev.push(info.row); + } + return prev; + }, []); + }, + + isRowExpanded(row) { + const { expandRows = [], rowKey } = this.states; + if (rowKey) { + const expandMap = getKeysMap(expandRows, rowKey); + return !!expandMap[getRowIdentity(row, rowKey)]; + } + return expandRows.indexOf(row) !== -1; + } + } +}; diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js new file mode 100644 index 00000000000..80cb5356934 --- /dev/null +++ b/packages/table/src/store/index.js @@ -0,0 +1,180 @@ +import Vue from 'vue'; +import Wachter from './watcher'; +import { arrayFind } from 'element-ui/src/utils/util'; + +Wachter.prototype.mutations = { + setData(states, data) { + const dataInstanceChanged = states._data !== data; + states._data = data; + + this.execQuery(); + // currentRow 更新 + this.updateCurrentRow(); + // expandRows 更新 + this.updateExpandRows(); + // 选择 + if (!states.reserveSelection) { + if (dataInstanceChanged) { + this.clearSelection(); + } else { + this.cleanSelection(); + } + } else { + this.assertRowkey(); + this.updateSelectionByRowKey(); + } + this.updateAllSelected(); + + this.modifiers.updateScrollY(); + }, + + insertColumn(states, column, index, parent) { + 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 === 'selection') { + states.selectable = column.selectable; + states.reserveSelection = column.reserveSelection; + } + + if (this.table.$ready) { + this.updateColumns(); // hack for dynamics insert column + 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.updateColumns(); // hack for dynamics remove column + this.scheduleLayout(); + } + }, + + sort(states, options) { + const { prop, order } = options; + if (prop) { + // TODO:nextTick 是否有必要? + Vue.nextTick(() => { + const column = arrayFind(states.columns, column => column.property === prop); + if (column) { + column.order = order; + this.updateSort(column, prop, order); + this.commit('changeSortCondition'); + } + }); + } + }, + + changeSortCondition(states, options) { + const ingore = { filter: true }; + this.execQuery(ingore); + + if (!options || !options.silent) { + this.table.$emit('sort-change', { + column: this.states.sortingColumn, + prop: this.states.sortProp, + order: this.states.sortOrder + }); + } + + this.modifiers.updateScrollY(); + }, + + filterChange(states, options) { + let { column, values, silent } = options; + const newFilters = this.updateFilters(column, values); + + this.execQuery(); + + if (!silent) { + this.table.$emit('filter-change', newFilters); + } + + this.modifiers.updateScrollY(); + }, + + toggleAllSelection() { + this.toggleAllSelection(); + }, + + rowSelectedChanged(states, row) { + this.toggleRowSelection(row); + this.updateAllSelected(); + }, + + // throttle + setHoverRow(states, row) { + states.hoverRow = row; + }, + + // throttle + setCurrentRow(states, row) { + const oldCurrentRow = states.currentRow; + states.currentRow = row; + + if (oldCurrentRow !== row) { + this.table.$emit('current-change', row, oldCurrentRow); + } + } +}; + +Wachter.prototype.commit = function(name, ...args) { + const mutations = this.mutations; + if (mutations[name]) { + mutations[name].apply(this, [this.states].concat(args)); + } else { + throw new Error(`Action not found: ${name}`); + } +}; + +// 这样做是否 +Wachter.prototype.modifiers = { + updateScrollY() { + Vue.nextTick(() => { + this.table.updateScrollY(); + }); + }, + + updateCurrentRow() { + console.log('todo ===> '); + } +}; + +export function createStore(table, initialState = {}) { + if (!table) { + throw new Error('Table is required.'); + } + + const store = new Wachter(); + + store.table = table; + Object.keys(initialState).forEach(key => { + store.states[key] = initialState[key]; + }); + + // 绑定一下 this + Object.keys(store.modifiers).forEach(key => { + store.modifiers[key] = store.modifiers[key].bind(store); + }); + + return store; +} + diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js new file mode 100644 index 00000000000..d339d192cec --- /dev/null +++ b/packages/table/src/store/tree.js @@ -0,0 +1,44 @@ +// import { getKeysMap, getRowIdentity, walkTreeNode } from '../util'; + +export default { + data() { + return { + states: { + // defaultExpandAll 存在于 expand.js 中,在这里只是注释掉。 + // TODO:拆分为独立的 TreeTale + // defaultExpandAll: false, + treeData: {}, + indent: 16, + lazy: false, + lazyTreeNodeMap: {}, + childrenColumnName: 'children' + } + }; + }, + + methods: { + updateTreeData() { + // 这里要处理一下数据合并的情形 + this.assertRowKey(); + + // const { states, table } = this; + // const { data = [], rowKey } = states; + // const newTreeData = {}; + // const oldTreeData = states.treeData; + // walkTreeNode(data, (parent, children, level) => { + // }, this.childrenColumnName); + }, + + updateTreeExpansion() { + + }, + + toggleTreeExpansion() { + + }, + + loadData() { + + } + } +}; diff --git a/packages/table/src/store/watcher.js b/packages/table/src/store/watcher.js new file mode 100644 index 00000000000..d4171359659 --- /dev/null +++ b/packages/table/src/store/watcher.js @@ -0,0 +1,371 @@ +import Vue from 'vue'; +import debounce from 'throttle-debounce/debounce'; +import merge from 'element-ui/src/utils/merge'; +import { getKeysMap, getRowIdentity, getColumnById, getColumnByKey, orderBy, toggleRowStatus } from '../util'; +import expand from './expand'; +import current from './current'; + +// import +const sortData = (data, states) => { + const sortingColumn = states.sortingColumn; + if (!sortingColumn || typeof sortingColumn.sortable === 'string') { + return data; + } + return orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy); +}; + +const doFlattenColumns = (columns) => { + const result = []; + columns.forEach((column) => { + if (column.children) { + result.push.apply(result, doFlattenColumns(column.children)); + } else { + result.push(column); + } + }); + return result; +}; + +export default Vue.extend({ + data() { + return { + states: { + // 3.0 版本后要求必须设置该属性 + rowKey: null, + + // 渲染的数据来源,是对 table 中的 data 过滤排序后的结果 + data: [], + + // 是否包含固定列 + isComplex: false, + + // 列 + _columns: [], // 不可响应的 + originColumns: [], + columns: [], + fixedColumns: [], + rightFixedColumns: [], + leafColumns: [], + fixedLeafColumns: [], + rightFixedLeafColumns: [], + leafColumnsLength: 0, + fixedLeafColumnsLength: 0, + rightFixedLeafColumnsLength: 0, + + // 选择 + isAllSelected: false, + selection: [], + reserveSelection: false, + selectOnIndeterminate: false, + selectable: null, + + // 过滤 + filters: {}, // 不可响应的 + filteredData: null, + + // 排序 + sortingColumn: null, + sortProp: null, + sortOrder: null, + + hoverRow: null, + currentRow: null + } + }; + }, + + mixins: [expand, current], + + methods: { + // 检查 rowKey 是否存在 + assertRowKey() { + const rowKey = this.states.rowKey; + if (!rowKey) throw new Error('[ElTable] prop row-key is required'); + }, + + // 更新列 + updateColumns() { + const states = this.states; + const _columns = states._columns || []; + states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left'); + states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right'); + + if (states.fixedColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) { + _columns[0].fixed = true; + states.fixedColumns.unshift(_columns[0]); + } + + const notFixedColumns = _columns.filter(column => !column.fixed); + states.originColumns = [].concat(states.fixedColumns).concat(notFixedColumns).concat(states.rightFixedColumns); + + const leafColumns = doFlattenColumns(notFixedColumns); + const fixedLeafColumns = doFlattenColumns(states.fixedColumns); + const rightFixedLeafColumns = doFlattenColumns(states.rightFixedColumns); + + states.leafColumnsLength = leafColumns.length; + states.fixedLeafColumnsLength = fixedLeafColumns.length; + states.rightFixedLeafColumnsLength = rightFixedLeafColumns.length; + + states.columns = [].concat(fixedLeafColumns).concat(leafColumns).concat(rightFixedLeafColumns); + states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0; + }, + + // 更新 DOM + scheduleLayout(needUpdateColumns) { + if (needUpdateColumns) { + this.updateColumns(); + } + this.table.debouncedUpdateLayout(); + }, + + // 选择 + isSelected(row) { + const { selection = [] } = this.states; + return selection.indexOf(row) > -1; + }, + + clearSelection() { + const states = this.states; + states.isAllSelected = false; + const oldSelection = states.selection; + if (states.selection.length) { + states.selection = []; + } + if (oldSelection.length > 0) { + this.table.$emit('selection-change', states.selection ? states.selection.slice() : []); + } + }, + + cleanSelection() { + const selection = this.states.selection || []; + const data = this.states.data; + const rowKey = this.states.rowKey; + let deleted; + if (rowKey) { + deleted = []; + const selectedMap = getKeysMap(selection, rowKey); + const dataMap = getKeysMap(data, rowKey); + for (let key in selectedMap) { + if (selectedMap.hasOwnProperty(key) && !dataMap[key]) { + deleted.push(selectedMap[key].row); + } + } + } else { + deleted = selection.filter((item) => { + return data.indexOf(item) === -1; + }); + } + + deleted.forEach((deletedItem) => { + selection.splice(selection.indexOf(deletedItem), 1); + }); + + if (deleted.length) { + this.table.$emit('selection-change', selection ? selection.slice() : []); + } + }, + + toggleRowSelection(row, selected) { + const changed = toggleRowStatus(this.states.selection, row, selected); + if (changed) { + const newSelection = this.states.selection ? this.states.selection.slice() : []; + this.table.$emit('select', newSelection, row); + this.table.$emit('selection-change', newSelection); + } + }, + + toggleAllSelection: debounce(10, function() { + const states = this.states; + const { data = [], selection } = states; + // when only some rows are selected (but not all), select or deselect all of them + // depending on the value of selectOnIndeterminate + const value = states.selectOnIndeterminate + ? !states.isAllSelected + : !(states.isAllSelected || selection.length); + states.isAllSelected = value; + + let selectionChanged = false; + data.forEach((row, index) => { + if (states.selectable) { + if (states.selectable.call(null, row, index) && toggleRowStatus(selection, row, value)) { + selectionChanged = true; + } + } else { + if (toggleRowStatus(selection, row, value)) { + selectionChanged = true; + } + } + }); + + if (selectionChanged) { + this.table.$emit('selection-change', selection ? selection.slice() : []); + } + this.table.$emit('select-all', selection); + }), + + updateSelectionByRowKey() { + const states = this.states; + const { selection, rowKey, data = [] } = states; + const selectedMap = getKeysMap(selection, rowKey); + // TODO:这里的代码可以优化 + states.selection = data.reduce((prev, row) => { + const rowId = getRowIdentity(row, rowKey); + const rowInfo = selectedMap[rowId]; + if (rowInfo) { + prev.push(row); + } + return prev; + }, []); + }, + + updateAllSelected() { + const states = this.states; + const { selection, rowKey, selectable } = states; + // data 为 null 时,结构时的默认值会被忽略 + const data = states.data || []; + if (data.length === 0) { + states.isAllSelected = false; + return; + } + + let selectedMap; + if (rowKey) { + selectedMap = getKeysMap(selection, rowKey); + } + const isSelected = function(row) { + if (selectedMap) { + return !!selectedMap[getRowIdentity(row, rowKey)]; + } else { + return selection.indexOf(row) !== -1; + } + }; + let isAllSelected = true; + let selectedCount = 0; + for (let i = 0, j = data.length; i < j; i++) { + const item = data[i]; + const isRowSelectable = selectable && selectable.call(null, item, i); + if (!isSelected(item)) { + if (!selectable || isRowSelectable) { + isAllSelected = false; + break; + } + } else { + selectedCount++; + } + } + + if (selectedCount === 0) isAllSelected = false; + states.isAllSelected = isAllSelected; + }, + + // 过滤与排序 + updateFilters(columns, values) { + if (!Array.isArray(columns)) { + columns = [columns]; + } + const states = this.states; + const filters = {}; + columns.forEach(col => { + states.filters[col.id] = values; + filters[col.columnKey || col.id] = values; + }); + + return filters; + }, + + updateSort(column, prop, order) { + this.states.sortingColumn = column; + this.states.sortProp = prop; + this.states.sortOrder = order; + }, + + execFilter() { + const states = this.states; + const { _data, filters } = states; + let data = _data; + + Object.keys(filters).forEach((columnId) => { + const values = states.filters[columnId]; + if (!values || values.length === 0) return; + const column = getColumnById(this.states, columnId); + if (column && column.filterMethod) { + data = data.filter((row) => { + return values.some(value => column.filterMethod.call(null, value, row, column)); + }); + } + }); + + states.filteredData = data; + // states.data = data; + }, + + execSort() { + const states = this.states; + states.data = sortData(states.filteredData, states); + }, + + // 根据 filters 与 sort 去过滤 data + execQuery(ignore) { + if (!(ignore && ignore.filter)) { + this.execFilter(); + } + this.execSort(); + }, + + clearFilter(columnKeys) { + const states = this.states; + const { tableHeader, fixedTableHeader, rightFixedTableHeader } = this.table.$refs; + + let panels = {}; + if (tableHeader) panels = merge(panels, tableHeader.filterPanels); + if (fixedTableHeader) panels = merge(panels, fixedTableHeader.filterPanels); + if (rightFixedTableHeader) panels = merge(panels, rightFixedTableHeader.filterPanels); + + const keys = Object.keys(panels); + if (!keys.length) return; + + if (typeof columnKeys === 'string') { + columnKeys = [columnKeys]; + } + + if (Array.isArray(columnKeys)) { + const columns = columnKeys.map(key => getColumnByKey(states, key)); + keys.forEach(key => { + const column = columns.find(col => col.id === key); + if (column) { + // TODO: 优化这里的代码 + panels[key].filteredValue = []; + } + }); + this.commit('filterChange', { + column: columns, + values: [], + silent: true, + multi: true + }); + } else { + keys.forEach(key => { + // TODO: 优化这里的代码 + panels[key].filteredValue = []; + }); + + states.filters = {}; + this.commit('filterChange', { + column: {}, + values: [], + silent: true + }); + } + }, + + clearSort() { + const states = this.states; + if (!states.sortingColumn) return; + + this.updateSort(null, null, null); + this.commit('changeSortCondition', { + silent: true + }); + } + } +}); diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 1a5720ceace..6a7d113d3af 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -28,30 +28,30 @@ export default { }, render(h) { - const columnsHidden = this.columns.map((column, index) => this.isColumnHidden(index)); - let rows = this.data; - if (this.store.states.lazy && Object.keys(this.store.states.lazyTreeNodeMap).length) { - rows = rows.reduce((prev, item) => { - prev.push(item); - const rowKey = this.store.table.getRowKey(item); - const parent = this.store.states.treeData[rowKey]; - if (parent && parent.children && parent.hasChildren) { - const tmp = []; - const traverse = (children) => { - if (!children) return; - children.forEach(key => { - tmp.push(this.store.states.lazyTreeNodeMap[key]); - if (this.store.states.treeData[key]) { - traverse(this.store.states.treeData[key].children); - } - }); - }; - traverse(parent.children); - prev = prev.concat(tmp); - } - return prev; - }, []); - } + const data = this.data || []; + // let rows = this.data; + // if (this.store.states.lazy && Object.keys(this.store.states.lazyTreeNodeMap).length) { + // rows = rows.reduce((prev, item) => { + // prev.push(item); + // const rowKey = this.store.table.getRowKey(item); + // const parent = this.store.states.treeData[rowKey]; + // if (parent && parent.children && parent.hasChildren) { + // const tmp = []; + // const traverse = (children) => { + // if (!children) return; + // children.forEach(key => { + // tmp.push(this.store.states.lazyTreeNodeMap[key]); + // if (this.store.states.treeData[key]) { + // traverse(this.store.states.treeData[key].children); + // } + // }); + // }; + // traverse(parent.children); + // prev = prev.concat(tmp); + // } + // return prev; + // }, []); + // } return ( { - this._l(rows, (row, $index) => { - const rowKey = this.table.rowKey ? this.getKeyOfRow(row, $index) : $index; - const treeNode = this.treeData[rowKey]; - const rowClasses = this.getRowClass(row, $index); - if (treeNode) { - rowClasses.push('el-table__row--level-' + treeNode.level); - } - const tr = ( 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() } - class={ rowClasses }> - { - this._l(this.columns, (column, cellIndex) => { - const { rowspan, colspan } = this.getSpan(row, column, $index, cellIndex); - if (!rowspan || !colspan) { - return ''; - } else { - const columnData = { ...column }; - if (colspan !== 1) { - columnData.realWidth = columnData.realWidth * colspan; - } - const data = { - store: this.store, - _self: this.context || this.table.$vnode.context, - column: columnData, - row, - $index - }; - if (cellIndex === this.firstDefaultColumnIndex && treeNode) { - data.treeNode = { - hasChildren: treeNode.hasChildren || (treeNode.children && treeNode.children.length), - expanded: treeNode.expanded, - indent: treeNode.level * this.treeIndent, - level: treeNode.level, - loaded: treeNode.loaded, - rowKey - }; - } - return ( - - ); - } - }) - } - ); + data.map((row, $index) => { + const tr = this.rowRender(row, $index); if (this.hasExpandColumn && this.store.isRowExpanded(row)) { return [ tr, @@ -143,10 +79,9 @@ export default { } else { return tr; } - }).concat( - - ) + }) } +
this.handleCellMouseEnter($event, row) } - on-mouseleave={ this.handleCellMouseLeave }> - { - column.renderCell.call( - this._renderProxy, - h, - data, - columnsHidden[cellIndex] - ) - } -
); @@ -161,9 +96,9 @@ export default { return this.store.states.data; }, - treeData() { - return this.store.states.treeData; - }, + // treeData() { + // return this.store.states.treeData; + // }, columnsCount() { return this.store.states.columns.length; @@ -191,20 +126,20 @@ export default { hasExpandColumn() { return this.columns.some(({ type }) => type === 'expand'); - }, - - firstDefaultColumnIndex() { - for (let index = 0; index < this.columns.length; index++) { - if (this.columns[index].type === 'default') { - return index; - } - } - return 0; - }, - - treeIndent() { - return this.store.states.indent; } + + // firstDefaultColumnIndex() { + // for (let index = 0; index < this.columns.length; index++) { + // if (this.columns[index].type === 'default') { + // return index; + // } + // } + // return 0; + // }, + + // treeIndent() { + // return this.store.states.indent; + // } }, watch: { @@ -438,6 +373,52 @@ export default { handleExpandClick(row, e) { e.stopPropagation(); this.store.toggleRowExpansion(row); + }, + + rowRender(row, $index) { + const columnsHidden = this.columns.map((column, index) => this.isColumnHidden(index)); + return ( 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() } + class={ [this.getRowClass(row, $index)] }> + { + this.columns.map((column, cellIndex) => { + const { rowspan, colspan } = this.getSpan(row, column, $index, cellIndex); + if (!rowspan || !colspan) { + return null; + } + return ( + this.handleCellMouseEnter($event, row) } + on-mouseleave={ this.handleCellMouseLeave }> + { + column.renderCell.call( + this._renderProxy, + this.$createElement, + { + row, + column, + $index, + store: this.store, + _self: this.context || this.table.$vnode.context + }, + columnsHidden[cellIndex] + ) + } + + ); + }) + } + ); } } }; diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js index 96773c121b2..626daf7002e 100644 --- a/packages/table/src/table-column.js +++ b/packages/table/src/table-column.js @@ -1,141 +1,9 @@ +import { cellStarts, cellForced, defaultRenderCell } from './config'; +import { mergeOptions, parseWidth, parseMinWidth, compose } from './util'; import ElCheckbox from 'element-ui/packages/checkbox'; -import ElTag from 'element-ui/packages/tag'; -import objectAssign from 'element-ui/src/utils/merge'; -import { getPropByPath } from 'element-ui/src/utils/util'; let columnIdSeed = 1; -const defaults = { - default: { - order: '' - }, - selection: { - width: 48, - minWidth: 48, - realWidth: 48, - order: '', - className: 'el-table-column--selection' - }, - expand: { - width: 48, - minWidth: 48, - realWidth: 48, - order: '' - }, - index: { - width: 48, - minWidth: 48, - realWidth: 48, - order: '' - } -}; - -const forced = { - selection: { - renderHeader: function(h, { store }) { - return 0 && !this.isAllSelected } - nativeOn-click={ this.toggleAllSelection } - value={ this.isAllSelected } />; - }, - renderCell: function(h, { row, column, store, $index }) { - return event.stopPropagation() } - value={ store.isSelected(row) } - disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false } - on-input={ () => { store.commit('rowSelectedChanged', row); } } />; - }, - sortable: false, - resizable: false - }, - index: { - renderHeader: function(h, { column }) { - return column.label || '#'; - }, - renderCell: function(h, { $index, column }) { - let i = $index + 1; - const index = column.index; - - if (typeof index === 'number') { - i = $index + index; - } else if (typeof index === 'function') { - i = index($index); - } - - return
{ i }
; - }, - sortable: false - }, - expand: { - renderHeader: function(h, { column }) { - return column.label || ''; - }, - renderCell: function(h, { row, store }, proxy) { - const expanded = store.states.expandRows.indexOf(row) > -1; - return
proxy.handleExpandClick(row, e) }> - -
; - }, - sortable: false, - resizable: false, - className: 'el-table__expand-column' - } -}; - -const getDefaultColumn = function(type, options) { - const column = {}; - - objectAssign(column, defaults[type || 'default']); - - for (let name in options) { - if (options.hasOwnProperty(name)) { - const value = options[name]; - if (typeof value !== 'undefined') { - column[name] = value; - } - } - } - - if (!column.minWidth) { - column.minWidth = 80; - } - - column.realWidth = column.width === undefined ? column.minWidth : column.width; - - return column; -}; - -const DEFAULT_RENDER_CELL = function(h, { row, column, $index }) { - const property = column.property; - const value = property && getPropByPath(row, property).v; - if (column && column.formatter) { - return column.formatter(row, column, value, $index); - } - return value; -}; - -const parseWidth = (width) => { - if (width !== undefined) { - width = parseInt(width, 10); - if (isNaN(width)) { - width = null; - } - } - return width; -}; - -const parseMinWidth = (minWidth) => { - if (minWidth !== undefined) { - minWidth = parseInt(minWidth, 10); - if (isNaN(minWidth)) { - minWidth = 80; - } - } - return minWidth; -}; - export default { name: 'ElTableColumn', @@ -153,7 +21,7 @@ export default { minWidth: {}, renderHeader: Function, sortable: { - type: [String, Boolean], + type: [Boolean, String], default: false }, sortMethod: Function, @@ -162,7 +30,6 @@ export default { type: Boolean, default: true }, - context: {}, columnKey: String, align: String, headerAlign: String, @@ -199,17 +66,6 @@ export default { }; }, - beforeCreate() { - this.row = {}; - this.column = {}; - this.$index = 0; - }, - - components: { - ElCheckbox, - ElTag - }, - computed: { owner() { let parent = this.$parent; @@ -218,235 +74,175 @@ export default { } return parent; }, + columnOrTableParent() { let parent = this.$parent; while (parent && !parent.tableId && !parent.columnId) { parent = parent.$parent; } return parent; - } - }, - - created() { - this.customRender = this.$options.render; - this.$options.render = h => h('div', this.$slots.default); - - let parent = this.columnOrTableParent; - let owner = this.owner; - this.isSubColumn = owner !== parent; - this.columnId = (parent.tableId || parent.columnId) + '_column_' + columnIdSeed++; - - let type = this.type; - - const width = parseWidth(this.width); - const minWidth = parseMinWidth(this.minWidth); - - let isColumnGroup = false; - - let column = getDefaultColumn(type, { - id: this.columnId, - columnKey: this.columnKey, - label: this.label, - className: this.className, - labelClassName: this.labelClassName, - property: this.prop || this.property, - type, - renderCell: null, - renderHeader: this.renderHeader, - minWidth, - width, - isColumnGroup, - context: this.context, - align: this.align ? 'is-' + this.align : null, - headerAlign: this.headerAlign ? 'is-' + this.headerAlign : (this.align ? 'is-' + this.align : null), - sortable: this.sortable === '' ? true : this.sortable, - sortMethod: this.sortMethod, - sortBy: this.sortBy, - resizable: this.resizable, - showOverflowTooltip: this.showOverflowTooltip || this.showTooltipWhenOverflow, - formatter: this.formatter, - selectable: this.selectable, - reserveSelection: this.reserveSelection, - fixed: this.fixed === '' ? true : this.fixed, - filterMethod: this.filterMethod, - filters: this.filters, - filterable: this.filters || this.filterMethod, - filterMultiple: this.filterMultiple, - filterOpened: false, - filteredValue: this.filteredValue || [], - filterPlacement: this.filterPlacement || '', - index: this.index, - sortOrders: this.sortOrders - }); - - let source = forced[type] || {}; - Object.keys(source).forEach((prop) => { - let value = source[prop]; - if (value !== undefined) { - if (prop === 'renderHeader') { - if (type === 'selection' && column[prop]) { - console.warn('[Element Warn][TableColumn]Selection column doesn\'t allow to set render-header function.'); - } else { - value = column[prop] || value; - } - } - column[prop] = prop === 'className' ? `${column[prop]} ${value}` : value; - } - }); - - // Deprecation warning for renderHeader property - if (this.renderHeader) { - console.warn('[Element Warn][TableColumn]Comparing to render-header, scoped-slot header is easier to use. We recommend users to use scoped-slot header.'); - } - - this.columnConfig = column; - - let renderCell = column.renderCell; - let _self = this; - - if (type === 'expand') { - owner.renderExpanded = function(h, data) { - return _self.$scopedSlots.default - ? _self.$scopedSlots.default(data) - : _self.$slots.default; - }; - - column.renderCell = function(h, data) { - return
{ renderCell(h, data, this._renderProxy) }
; - }; - - return; - } - - column.renderCell = function(h, data) { - if (_self.$scopedSlots.default) { - renderCell = () => _self.$scopedSlots.default(data); - } - - if (!renderCell) { - renderCell = DEFAULT_RENDER_CELL; - } - const children = [ - _self.renderTreeCell(data), - renderCell(h, data) - ]; - - return _self.showOverflowTooltip || _self.showTooltipWhenOverflow - ?
{ children }
- : (
- { children } -
); - }; - }, - - destroyed() { - if (!this.$parent) return; - const parent = this.$parent; - this.owner.store.commit('removeColumn', this.columnConfig, this.isSubColumn ? parent.columnConfig : null); - }, - - watch: { - label(newVal) { - if (this.columnConfig) { - this.columnConfig.label = newVal; - } - }, - - prop(newVal) { - if (this.columnConfig) { - this.columnConfig.property = newVal; - } }, - property(newVal) { - if (this.columnConfig) { - this.columnConfig.property = newVal; - } + realWidth() { + return parseWidth(this.width); }, - filters(newVal) { - if (this.columnConfig) { - this.columnConfig.filters = newVal; - } + realMinWidth() { + return parseMinWidth(this.minWidth); }, - filterMultiple(newVal) { - if (this.columnConfig) { - this.columnConfig.filterMultiple = newVal; - } + realAlign() { + return this.align ? 'is-' + this.align : null; }, - align(newVal) { - if (this.columnConfig) { - this.columnConfig.align = newVal ? 'is-' + newVal : null; + realHeaderAlign() { + return this.headerAlign ? 'is-' + this.headerAlign : this.realAlign; + } + }, - if (!this.headerAlign) { - this.columnConfig.headerAlign = newVal ? 'is-' + newVal : null; + methods: { + getPropsData(...props) { + return props.reduce((prev, cur) => { + if (Array.isArray(cur)) { + cur.forEach((key) => { + prev[key] = this[key]; + }); } - } + return prev; + }, {}); }, - headerAlign(newVal) { - if (this.columnConfig) { - this.columnConfig.headerAlign = 'is-' + (newVal ? newVal : this.align); - } + getColumnElIndex(children, child) { + return [].indexOf.call(children, child); }, - width(newVal) { - if (this.columnConfig) { - this.columnConfig.width = parseWidth(newVal); - this.owner.store.scheduleLayout(); + setColumnWidth(column) { + if (this.realWidth) { + column.width = this.realWidth; } - }, - - minWidth(newVal) { - if (this.columnConfig) { - this.columnConfig.minWidth = parseMinWidth(newVal); - this.owner.store.scheduleLayout(); + if (this.realMinWidth) { + column.minWidth = this.realMinWidth; } - }, - - fixed(newVal) { - if (this.columnConfig) { - this.columnConfig.fixed = newVal; - this.owner.store.scheduleLayout(true); + if (!column.minWidth) { + column.minWidth = 80; } + column.realWidth = column.width === undefined ? column.minWidth : column.width; + return column; }, - sortable(newVal) { - if (this.columnConfig) { - this.columnConfig.sortable = newVal; - } + setColumnForcedProps(column) { + // 对于特定类型的 column,某些属性不允许设置 + const type = column.type; + const source = cellForced[type] || {}; + Object.keys(source).forEach(prop => { + let value = source[prop]; + if (value !== undefined) { + column[prop] = prop === 'className' ? `${column[prop]} ${value}` : value; + } + }); + return column; }, - index(newVal) { - if (this.columnConfig) { - this.columnConfig.index = newVal; + setColumnRenders(column) { + const specialTypes = Object.keys(cellForced); + // renderHeader 属性不推荐使用。 + if (this.renderHeader) { + console.warn('[Element Warn][TableColumn]Comparing to render-header, scoped-slot header is easier to use. We recommend users to use scoped-slot header.'); + } else if (specialTypes.indexOf(column.type) === -1) { + column.renderHeader = (h, scope) => { + const renderHeader = this.$scopedSlots.header; + return renderHeader ? renderHeader(scope) : column.label; + }; } - }, - formatter(newVal) { - if (this.columnConfig) { - this.columnConfig.formatter = newVal; + let originRenderCell = column.renderCell; + // TODO: 这里的实现调整 + if (column.type === 'expand') { + // 对于展开行,renderCell 不允许配置的。在上一步中已经设置过,这里需要简单封装一下。 + column.renderCell = (h, data) => (
+ { originRenderCell(h, data) } +
); + this.owner.renderExpanded = (h, data) => { + return this.$scopedSlots.default + ? this.$scopedSlots.default(data) + : this.$slots.default; + }; + } else { + originRenderCell = originRenderCell || defaultRenderCell; + // 对 renderCell 进行包装 + column.renderCell = (h, data) => { + let children = null; + if (this.$scopedSlots.default) { + children = this.$scopedSlots.default(data); + } else { + children = originRenderCell(h, data); + } + const props = { + class: 'cell', + style: {} + }; + if (column.showOverflowTooltip) { + props.class += ' el-tooltip'; + props.style = {width: (data.column.realWidth || data.column.width) - 1 + 'px'}; + } + return (
+ { this.renderTreeCell(data) } + { children } +
); + }; } + return column; }, - className(newVal) { - if (this.columnConfig) { - this.columnConfig.className = newVal; - } + registerNormalWatchers() { + const props = ['label', 'property', 'filters', 'filterMultiple', 'sortable', 'index', 'formatter', 'className', 'labelClassName']; + // 一些属性具有别名 + const aliases = { + prop: 'property', + realAlign: 'align', + realHeaderAlign: 'headerAlign', + realWidth: 'width' + }; + const allAliases = props.reduce((prev, cur) => { + prev[cur] = cur; + return prev; + }, aliases); + + Object.keys(allAliases).forEach(key => { + const columnKey = aliases[key]; + + this.$watch(key, (newVal) => { + this.columnConfig[columnKey] = newVal; + }); + }); }, - labelClassName(newVal) { - if (this.columnConfig) { - this.columnConfig.labelClassName = newVal; - } - } - }, + registerComplexWatchers() { + const props = ['fixed']; + const aliases = { + realWidth: 'width', + realMinWidth: 'minWidth' + }; + const allAliases = props.reduce((prev, cur) => { + prev[cur] = cur; + return prev; + }, aliases); + + Object.keys(allAliases).forEach(key => { + const columnKey = aliases[key]; + + this.$watch(key, (newVal) => { + this.columnConfig[columnKey] = newVal; + const updateColumns = columnKey === 'fixed'; + this.owner.store.scheduleLayout(updateColumns); + }); + }); + }, - methods: { + // TODO: 移除这里的实现 renderTreeCell(data) { if (!data.treeNode) return null; + console.warn('tree 的相关逻辑要调整'); const ele = []; ele.push(); if (data.treeNode.hasChildren) { @@ -470,25 +266,78 @@ export default { } }, + components: { + ElCheckbox + }, + + beforeCreate() { + this.row = {}; + this.column = {}; + this.$index = 0; + this.columnId = ''; + }, + + created() { + const parent = this.columnOrTableParent; + this.isSubColumn = this.owner !== parent; + this.columnId = (parent.tableId || parent.columnId) + '_column_' + columnIdSeed++; + + const type = this.type || 'default'; + const sortable = this.sortable === '' ? true : this.sortable; + const defaults = { + ...cellStarts[type], + id: this.columnId, + type: type, + property: this.prop || this.property, + align: this.realAlign, + headerAlign: this.realHeaderAlign, + showOverflowTooltip: this.showOverflowTooltip || this.showTooltipWhenOverflow, + // filter 相关属性 + filterable: this.filters || this.filterMethod, + filteredValue: [], + filterPlacement: '', + isColumnGroup: false, + filterOpened: false, + // sort 相关属性 + sortable: sortable + }; + + const basicProps = ['columnKey', 'label', 'className', 'labelClassName', 'type', 'renderHeader', 'resizable', 'formatter', 'fixed', 'resizable']; + const sortProps = ['sortMethod', 'sortBy', 'sortOrders']; + const selectProps = ['selectable', 'reserveSelection']; + const filterProps = ['filterMethod', 'filters', 'filterMultiple', 'filterOpened', 'filteredValue', 'filterPlacement']; + + let column = this.getPropsData(basicProps, sortProps, selectProps, filterProps); + column = mergeOptions(defaults, column); + + // 注意 compose 中函数执行的顺序是从右到左 + const chains = compose(this.setColumnRenders, this.setColumnWidth, this.setColumnForcedProps); + column = chains(column); + + this.columnConfig = column; + + // 注册 watcher + this.registerNormalWatchers(); + this.registerComplexWatchers(); + }, + mounted() { const owner = this.owner; const parent = this.columnOrTableParent; - let columnIndex; + const children = this.isSubColumn ? parent.$el.children : parent.$refs.hiddenColumns.children; + const columnIndex = this.getColumnElIndex(children, this.$el); - if (!this.isSubColumn) { - columnIndex = [].indexOf.call(parent.$refs.hiddenColumns.children, this.$el); - } else { - columnIndex = [].indexOf.call(parent.$el.children, this.$el); - } + owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null); + }, - if (this.$scopedSlots.header) { - if (this.type === 'selection') { - console.warn('[Element Warn][TableColumn]Selection column doesn\'t allow to set scoped-slot header.'); - } else { - this.columnConfig.renderHeader = (h, scope) => this.$scopedSlots.header(scope); - } - } + destroyed() { + if (!this.$parent) return; + const parent = this.$parent; + this.owner.store.commit('removeColumn', this.columnConfig, this.isSubColumn ? parent.columnConfig : null); + }, - owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null); + render(h) { + // slots 也要渲染,需要计算合并表头 + return h('div', this.$slots.default); } }; diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index cddb708e229..3b718a42ea9 100644 --- a/packages/table/src/table-header.js +++ b/packages/table/src/table-header.js @@ -96,42 +96,40 @@ export default { class={ this.getHeaderRowClass(rowIndex) } > { - this._l(columns, (column, cellIndex) => - this.handleMouseMove($event, column) } - on-mouseout={ this.handleMouseOut } - on-mousedown={ ($event) => this.handleMouseDown($event, column) } - on-click={ ($event) => this.handleHeaderClick($event, column) } - on-contextmenu={ ($event) => this.handleHeaderContextMenu($event, column) } - style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) } - class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) } - key={ column.id }> -
0 ? 'highlight' : '', column.labelClassName] }> - { - column.renderHeader - ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }) - : column.label - } - { - column.sortable - ? this.handleSortClick($event, column) }> - this.handleSortClick($event, column, 'ascending') }> - - this.handleSortClick($event, column, 'descending') }> - - - : '' - } - { - column.filterable - ? this.handleFilterClick($event, column) }> - : '' - } -
- - ) + columns.map((column, cellIndex) => ( this.handleMouseMove($event, column) } + on-mouseout={ this.handleMouseOut } + on-mousedown={ ($event) => this.handleMouseDown($event, column) } + on-click={ ($event) => this.handleHeaderClick($event, column) } + on-contextmenu={ ($event) => this.handleHeaderContextMenu($event, column) } + style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) } + class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) } + key={ column.id }> +
0 ? 'highlight' : '', column.labelClassName] }> + { + column.renderHeader + ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }) + : column.label + } + { + column.sortable + ? this.handleSortClick($event, column) }> + this.handleSortClick($event, column, 'ascending') }> + + this.handleSortClick($event, column, 'descending') }> + + + : '' + } + { + column.filterable + ? this.handleFilterClick($event, column) }> + : '' + } +
+ )) } { this.hasGutter ? : '' diff --git a/packages/table/src/table-store.js b/packages/table/src/table-store.js deleted file mode 100644 index 54b7294afe6..00000000000 --- a/packages/table/src/table-store.js +++ /dev/null @@ -1,753 +0,0 @@ -import Vue from 'vue'; -import debounce from 'throttle-debounce/debounce'; -import merge from 'element-ui/src/utils/merge'; -import { orderBy, getColumnById, getRowIdentity, getColumnByKey } from './util'; - -const sortData = (data, states) => { - const sortingColumn = states.sortingColumn; - if (!sortingColumn || typeof sortingColumn.sortable === 'string') { - return data; - } - if (Object.keys(states.treeData).length === 0) { - return orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy); - } - // 存在嵌套类型的数据 - const rowKey = states.rowKey; - const filteredData = []; - const treeDataMap = {}; - let index = 0; - while (index < data.length) { - let cur = data[index]; - const key = cur[rowKey]; - let treeNode = states.treeData[key]; - filteredData.push(cur); - index++; - if (!treeNode) { - continue; - } - treeDataMap[key] = []; - while (index < data.length) { - cur = data[index]; - treeNode = states.treeData[cur[rowKey]]; - index++; - if (treeNode && treeNode.level !== 0) { - treeDataMap[key].push(cur); - } else { - filteredData.push(cur); - break; - } - } - } - const sortedData = orderBy(filteredData, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy); - return sortedData.reduce((prev, current) => { - const treeNodes = treeDataMap[current[rowKey]] || []; - return prev.concat(current, treeNodes); - }, []); -}; - -const getKeysMap = function(array, rowKey) { - const arrayMap = {}; - (array || []).forEach((row, index) => { - arrayMap[getRowIdentity(row, rowKey)] = { row, index }; - }); - return arrayMap; -}; - -const toggleRowSelection = function(states, row, selected) { - let changed = false; - const selection = states.selection; - const index = selection.indexOf(row); - if (typeof selected === 'undefined') { - if (index === -1) { - selection.push(row); - changed = true; - } else { - selection.splice(index, 1); - changed = true; - } - } else { - if (selected && index === -1) { - selection.push(row); - changed = true; - } else if (!selected && index > -1) { - selection.splice(index, 1); - changed = true; - } - } - - return changed; -}; - -const toggleRowExpansion = function(states, row, expanded) { - let changed = false; - const expandRows = states.expandRows; - if (typeof expanded !== 'undefined') { - const index = expandRows.indexOf(row); - if (expanded) { - if (index === -1) { - expandRows.push(row); - changed = true; - } - } else { - if (index !== -1) { - expandRows.splice(index, 1); - changed = true; - } - } - } else { - const index = expandRows.indexOf(row); - if (index === -1) { - expandRows.push(row); - changed = true; - } else { - expandRows.splice(index, 1); - changed = true; - } - } - - return changed; -}; - -const TableStore = function(table, initialState = {}) { - if (!table) { - throw new Error('Table is required.'); - } - this.table = table; - - this.states = { - rowKey: null, - _columns: [], - originColumns: [], - columns: [], - fixedColumns: [], - rightFixedColumns: [], - leafColumns: [], - fixedLeafColumns: [], - rightFixedLeafColumns: [], - leafColumnsLength: 0, - fixedLeafColumnsLength: 0, - rightFixedLeafColumnsLength: 0, - isComplex: false, - filteredData: null, - data: null, - sortingColumn: null, - sortProp: null, - sortOrder: null, - isAllSelected: false, - selection: [], - reserveSelection: false, - selectable: null, - currentRow: null, - hoverRow: null, - filters: {}, - expandRows: [], - defaultExpandAll: false, - selectOnIndeterminate: false, - treeData: {}, - indent: 16, - lazy: false, - lazyTreeNodeMap: {} - }; - - this._toggleAllSelection = debounce(10, function(states) { - const data = states.data || []; - if (data.length === 0) return; - const selection = this.states.selection; - // when only some rows are selected (but not all), select or deselect all of them - // depending on the value of selectOnIndeterminate - const value = states.selectOnIndeterminate - ? !states.isAllSelected - : !(states.isAllSelected || selection.length); - let selectionChanged = false; - data.forEach((item, index) => { - if (states.selectable) { - if (states.selectable.call(null, item, index) && toggleRowSelection(states, item, value)) { - selectionChanged = true; - } - } else { - if (toggleRowSelection(states, item, value)) { - selectionChanged = true; - } - } - }); - const table = this.table; - if (selectionChanged) { - table.$emit('selection-change', selection ? selection.slice() : []); - } - table.$emit('select-all', selection); - states.isAllSelected = value; - }); - - for (let prop in initialState) { - if (initialState.hasOwnProperty(prop) && this.states.hasOwnProperty(prop)) { - this.states[prop] = initialState[prop]; - } - } -}; - -TableStore.prototype.mutations = { - setData(states, data) { - const dataInstanceChanged = states._data !== data; - states._data = data; - - Object.keys(states.filters).forEach((columnId) => { - const values = states.filters[columnId]; - if (!values || values.length === 0) return; - const column = getColumnById(this.states, columnId); - if (column && column.filterMethod) { - data = data.filter((row) => { - return values.some(value => column.filterMethod.call(null, value, row, column)); - }); - } - }); - - states.filteredData = data; - states.data = sortData((data || []), states); - - this.updateCurrentRow(); - - const rowKey = states.rowKey; - - if (!states.reserveSelection) { - if (dataInstanceChanged) { - this.clearSelection(); - } else { - this.cleanSelection(); - } - this.updateAllSelected(); - } else { - if (rowKey) { - const selection = states.selection; - const selectedMap = getKeysMap(selection, rowKey); - - states.data.forEach((row) => { - const rowId = getRowIdentity(row, rowKey); - const rowInfo = selectedMap[rowId]; - if (rowInfo) { - selection[rowInfo.index] = row; - } - }); - - this.updateAllSelected(); - } else { - console.warn('WARN: rowKey is required when reserve-selection is enabled.'); - } - } - - const defaultExpandAll = states.defaultExpandAll; - if (defaultExpandAll) { - this.states.expandRows = (states.data || []).slice(0); - } else if (rowKey) { - // update expandRows to new rows according to rowKey - const ids = getKeysMap(this.states.expandRows, rowKey); - let expandRows = []; - for (const row of states.data) { - const rowId = getRowIdentity(row, rowKey); - if (ids[rowId]) { - expandRows.push(row); - } - } - this.states.expandRows = expandRows; - } else { - // clear the old rows - this.states.expandRows = []; - } - - Vue.nextTick(() => this.table.updateScrollY()); - }, - - changeSortCondition(states, options) { - states.data = sortData((states.filteredData || states._data || []), states); - - if (!options || !(options.silent || options.init)) { - this.table.$emit('sort-change', { - column: this.states.sortingColumn, - prop: this.states.sortProp, - order: this.states.sortOrder - }); - } - - Vue.nextTick(() => this.table.updateScrollY()); - }, - - sort(states, options) { - const { prop, order, init } = options; - if (prop) { - states.sortProp = prop; - states.sortOrder = order || 'ascending'; - Vue.nextTick(() => { - for (let i = 0, length = states.columns.length; i < length; i++) { - let column = states.columns[i]; - if (column.property === states.sortProp) { - column.order = states.sortOrder; - states.sortingColumn = column; - break; - } - } - - if (states.sortingColumn) { - this.commit('changeSortCondition', { - init: init - }); - } - }); - } - }, - - filterChange(states, options) { - let { column, values, silent, multi } = options; - if (values && !Array.isArray(values)) { - values = [values]; - } - const filters = {}; - - if (multi) { - column.forEach(col => { - states.filters[col.id] = values; - filters[col.columnKey || col.id] = values; - }); - } else { - const prop = column.property; - - if (prop) { - states.filters[column.id] = values; - filters[column.columnKey || column.id] = values; - } - } - - let data = states._data; - - Object.keys(states.filters).forEach((columnId) => { - const values = states.filters[columnId]; - if (!values || values.length === 0) return; - const column = getColumnById(this.states, columnId); - if (column && column.filterMethod) { - data = data.filter((row) => { - return values.some(value => column.filterMethod.call(null, value, row, column)); - }); - } - }); - - states.filteredData = data; - states.data = sortData(data, states); - - if (!silent) { - this.table.$emit('filter-change', filters); - } - - Vue.nextTick(() => this.table.updateScrollY()); - }, - - insertColumn(states, column, index, parent) { - 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 === 'selection') { - states.selectable = column.selectable; - states.reserveSelection = column.reserveSelection; - } - - if (this.table.$ready) { - this.updateColumns(); // hack for dynamics insert column - 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.updateColumns(); // hack for dynamics remove column - this.scheduleLayout(); - } - }, - - setHoverRow(states, row) { - states.hoverRow = row; - }, - - setCurrentRow(states, row) { - const oldCurrentRow = states.currentRow; - states.currentRow = row; - - if (oldCurrentRow !== row) { - this.table.$emit('current-change', row, oldCurrentRow); - } - }, - - rowSelectedChanged(states, row) { - const changed = toggleRowSelection(states, row); - const selection = states.selection; - - if (changed) { - const table = this.table; - table.$emit('selection-change', selection ? selection.slice() : []); - table.$emit('select', selection, row); - } - - this.updateAllSelected(); - }, - - toggleAllSelection(state) { - this._toggleAllSelection(state); - } -}; - -const doFlattenColumns = (columns) => { - const result = []; - columns.forEach((column) => { - if (column.children) { - result.push.apply(result, doFlattenColumns(column.children)); - } else { - result.push(column); - } - }); - return result; -}; - -TableStore.prototype.updateColumns = function() { - const states = this.states; - const _columns = states._columns || []; - states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left'); - states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right'); - - if (states.fixedColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) { - _columns[0].fixed = true; - states.fixedColumns.unshift(_columns[0]); - } - - const notFixedColumns = _columns.filter(column => !column.fixed); - states.originColumns = [].concat(states.fixedColumns).concat(notFixedColumns).concat(states.rightFixedColumns); - - const leafColumns = doFlattenColumns(notFixedColumns); - const fixedLeafColumns = doFlattenColumns(states.fixedColumns); - const rightFixedLeafColumns = doFlattenColumns(states.rightFixedColumns); - - states.leafColumnsLength = leafColumns.length; - states.fixedLeafColumnsLength = fixedLeafColumns.length; - states.rightFixedLeafColumnsLength = rightFixedLeafColumns.length; - - states.columns = [].concat(fixedLeafColumns).concat(leafColumns).concat(rightFixedLeafColumns); - states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0; -}; - -TableStore.prototype.isSelected = function(row) { - return (this.states.selection || []).indexOf(row) > -1; -}; - -TableStore.prototype.clearSelection = function() { - const states = this.states; - states.isAllSelected = false; - const oldSelection = states.selection; - if (states.selection.length) { - states.selection = []; - } - if (oldSelection.length > 0) { - this.table.$emit('selection-change', states.selection ? states.selection.slice() : []); - } -}; - -TableStore.prototype.setExpandRowKeys = function(rowKeys) { - const expandRows = []; - const data = this.states.data; - const rowKey = this.states.rowKey; - if (!rowKey) throw new Error('[Table] prop row-key should not be empty.'); - const keysMap = getKeysMap(data, rowKey); - rowKeys.forEach((key) => { - const info = keysMap[key]; - if (info) { - expandRows.push(info.row); - } - }); - - this.states.expandRows = expandRows; -}; - -TableStore.prototype.toggleRowSelection = function(row, selected) { - const changed = toggleRowSelection(this.states, row, selected); - if (changed) { - this.table.$emit('selection-change', this.states.selection ? this.states.selection.slice() : []); - } -}; - -TableStore.prototype.toggleRowExpansion = function(row, expanded) { - const changed = toggleRowExpansion(this.states, row, expanded); - if (changed) { - this.table.$emit('expand-change', row, this.states.expandRows); - this.scheduleLayout(); - } -}; - -TableStore.prototype.isRowExpanded = function(row) { - const { expandRows = [], rowKey } = this.states; - if (rowKey) { - const expandMap = getKeysMap(expandRows, rowKey); - return !!expandMap[getRowIdentity(row, rowKey)]; - } - return expandRows.indexOf(row) !== -1; -}; - -TableStore.prototype.cleanSelection = function() { - const selection = this.states.selection || []; - const data = this.states.data; - const rowKey = this.states.rowKey; - let deleted; - if (rowKey) { - deleted = []; - const selectedMap = getKeysMap(selection, rowKey); - const dataMap = getKeysMap(data, rowKey); - for (let key in selectedMap) { - if (selectedMap.hasOwnProperty(key) && !dataMap[key]) { - deleted.push(selectedMap[key].row); - } - } - } else { - deleted = selection.filter((item) => { - return data.indexOf(item) === -1; - }); - } - - deleted.forEach((deletedItem) => { - selection.splice(selection.indexOf(deletedItem), 1); - }); - - if (deleted.length) { - this.table.$emit('selection-change', selection ? selection.slice() : []); - } -}; - -TableStore.prototype.clearFilter = function(columnKeys) { - const states = this.states; - const { tableHeader, fixedTableHeader, rightFixedTableHeader } = this.table.$refs; - let panels = {}; - - if (tableHeader) panels = merge(panels, tableHeader.filterPanels); - if (fixedTableHeader) panels = merge(panels, fixedTableHeader.filterPanels); - if (rightFixedTableHeader) panels = merge(panels, rightFixedTableHeader.filterPanels); - - const keys = Object.keys(panels); - if (!keys.length) return; - - if (typeof columnKeys === 'string') { - columnKeys = [columnKeys]; - } - if (Array.isArray(columnKeys)) { - const columns = columnKeys.map(key => getColumnByKey(states, key)); - keys.forEach(key => { - const column = columns.find(col => col.id === key); - if (column) { - panels[key].filteredValue = []; - } - }); - this.commit('filterChange', { - column: columns, - value: [], - silent: true, - multi: true - }); - } else { - keys.forEach(key => { - panels[key].filteredValue = []; - }); - - states.filters = {}; - - this.commit('filterChange', { - column: {}, - values: [], - silent: true - }); - } -}; - -TableStore.prototype.clearSort = function() { - const states = this.states; - if (!states.sortingColumn) return; - states.sortingColumn.order = null; - states.sortProp = null; - states.sortOrder = null; - - this.commit('changeSortCondition', { - silent: true - }); -}; - -TableStore.prototype.updateAllSelected = function() { - const states = this.states; - const { selection, rowKey, selectable, data } = states; - if (!data || data.length === 0) { - states.isAllSelected = false; - return; - } - - let selectedMap; - if (rowKey) { - selectedMap = getKeysMap(states.selection, rowKey); - } - - const isSelected = function(row) { - if (selectedMap) { - return !!selectedMap[getRowIdentity(row, rowKey)]; - } else { - return selection.indexOf(row) !== -1; - } - }; - - let isAllSelected = true; - let selectedCount = 0; - for (let i = 0, j = data.length; i < j; i++) { - const item = data[i]; - const isRowSelectable = selectable && selectable.call(null, item, i); - if (!isSelected(item)) { - if (!selectable || isRowSelectable) { - isAllSelected = false; - break; - } - } else { - selectedCount++; - } - } - - if (selectedCount === 0) isAllSelected = false; - - states.isAllSelected = isAllSelected; -}; - -TableStore.prototype.scheduleLayout = function(updateColumns) { - if (updateColumns) { - this.updateColumns(); - } - this.table.debouncedUpdateLayout(); -}; - -TableStore.prototype.setCurrentRowKey = function(key) { - const states = this.states; - const rowKey = states.rowKey; - if (!rowKey) throw new Error('[Table] row-key should not be empty.'); - const data = states.data || []; - const keysMap = getKeysMap(data, rowKey); - const info = keysMap[key]; - states.currentRow = info ? info.row : null; -}; - -TableStore.prototype.updateCurrentRow = function() { - const states = this.states; - const table = this.table; - const data = states.data || []; - const oldCurrentRow = states.currentRow; - - if (data.indexOf(oldCurrentRow) === -1) { - if (states.rowKey && oldCurrentRow) { - let newCurrentRow = null; - for (let i = 0; i < data.length; i++) { - const item = data[i]; - if (item && item[states.rowKey] === oldCurrentRow[states.rowKey]) { - newCurrentRow = item; - break; - } - } - if (newCurrentRow) { - states.currentRow = newCurrentRow; - return; - } - } - states.currentRow = null; - - if (states.currentRow !== oldCurrentRow) { - table.$emit('current-change', null, oldCurrentRow); - } - } -}; - -TableStore.prototype.commit = function(name, ...args) { - const mutations = this.mutations; - if (mutations[name]) { - mutations[name].apply(this, [this.states].concat(args)); - } else { - throw new Error(`Action not found: ${name}`); - } -}; - -TableStore.prototype.toggleTreeExpansion = function(rowKey) { - const { treeData } = this.states; - const node = treeData[rowKey]; - if (!node) return; - if (typeof node.expanded !== 'boolean') { - throw new Error('a leaf must have expanded property'); - } - node.expanded = !node.expanded; - - let traverse = null; - if (node.expanded) { - traverse = (children, parent) => { - if (children && parent.expanded) { - children.forEach(key => { - treeData[key].display = true; - traverse(treeData[key].children, treeData[key]); - }); - } - }; - node.children.forEach(key => { - treeData[key].display = true; - traverse(treeData[key].children, treeData[key]); - }); - } else { - const traverse = (children) => { - if (!children) return; - children.forEach(key => { - treeData[key].display = false; - traverse(treeData[key].children); - }); - }; - traverse(node.children); - } -}; - -TableStore.prototype.loadData = function(row, treeNode) { - const table = this.table; - const parentRowKey = treeNode.rowKey; - if (table.lazy && table.load) { - table.load(row, treeNode, (data) => { - if (!Array.isArray(data)) { - throw new Error('data must be an array'); - } - const treeData = this.states.treeData; - data.forEach(item => { - const rowKey = table.getRowKey(item); - const parent = treeData[parentRowKey]; - parent.loaded = true; - parent.children.push(rowKey); - const child = { - display: true, - level: parent.level + 1 - }; - if (item.hasChildren) { - child.expanded = false; - child.hasChildren = true; - child.children = []; - } - Vue.set(treeData, rowKey, child); - Vue.set(this.states.lazyTreeNodeMap, rowKey, item); - }); - this.toggleTreeExpansion(parentRowKey); - }); - } -}; - -export default TableStore; diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index 1908112243a..25024af169c 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -219,31 +219,11 @@ import Mousewheel from 'element-ui/src/directives/mousewheel'; import Locale from 'element-ui/src/mixins/locale'; import Migrating from 'element-ui/src/mixins/migrating'; - import TableStore from './table-store'; + import { createStore } from './store/index'; import TableLayout from './table-layout'; import TableBody from './table-body'; import TableHeader from './table-header'; import TableFooter from './table-footer'; - import { getRowIdentity } from './util'; - - const flattenData = function(data) { - if (!data) return data; - let newData = []; - const flatten = arr => { - arr.forEach((item) => { - newData.push(item); - if (Array.isArray(item.children)) { - flatten(item.children); - } - }); - }; - flatten(data); - if (data.length === newData.length) { - return data; - } else { - return newData; - } - }; let tableIdSeed = 1; @@ -480,61 +460,8 @@ toggleAllSelection() { this.store.commit('toggleAllSelection'); - }, - - getRowKey(row) { - const rowKey = getRowIdentity(row, this.store.states.rowKey); - if (!rowKey) { - throw new Error('if there\'s nested data, rowKey is required.'); - } - return rowKey; - }, - - getTableTreeData(data) { - const treeData = {}; - const traverse = (children, parentData, level) => { - children.forEach(item => { - const rowKey = this.getRowKey(item); - treeData[rowKey] = { - display: false, - level - }; - parentData.children.push(rowKey); - if (Array.isArray(item.children) && item.children.length) { - treeData[rowKey].children = []; - treeData[rowKey].expanded = false; - traverse(item.children, treeData[rowKey], level + 1); - } - }); - }; - if (data) { - data.forEach(item => { - const containChildren = Array.isArray(item.children) && item.children.length; - if (!(containChildren || item.hasChildren)) return; - const rowKey = this.getRowKey(item); - const treeNode = { - level: 0, - expanded: false, - display: true, - children: [] - }; - if (containChildren) { - treeData[rowKey] = treeNode; - traverse(item.children, treeData[rowKey], 1); - } else if (item.hasChildren && this.lazy) { - treeNode.hasChildren = true; - treeNode.loaded = false; - treeData[rowKey] = treeNode; - } - }); - } - return treeData; } - }, - created() { - this.tableId = 'el-table_' + tableIdSeed++; - this.debouncedUpdateLayout = debounce(50, () => this.doLayout()); }, computed: { @@ -658,8 +585,6 @@ data: { immediate: true, handler(value) { - this.store.states.treeData = this.getTableTreeData(value); - value = flattenData(value); this.store.commit('setData', value); if (this.$ready) { this.$nextTick(() => { @@ -679,8 +604,9 @@ } }, - destroyed() { - if (this.resizeListener) removeResizeListener(this.$el, this.resizeListener); + created() { + this.tableId = 'el-table_' + tableIdSeed++; + this.debouncedUpdateLayout = debounce(50, () => this.doLayout()); }, mounted() { @@ -707,23 +633,35 @@ this.$ready = true; }, + destroyed() { + if (this.resizeListener) removeResizeListener(this.$el, this.resizeListener); + }, + data() { - const store = new TableStore(this, { + this.store = createStore(this, { rowKey: this.rowKey, defaultExpandAll: this.defaultExpandAll, selectOnIndeterminate: this.selectOnIndeterminate, indent: this.indent, lazy: this.lazy }); + // const store = new TableStore(this, { + // rowKey: this.rowKey, + // defaultExpandAll: this.defaultExpandAll, + // selectOnIndeterminate: this.selectOnIndeterminate, + // indent: this.indent, + // lazy: this.lazy + // }); const layout = new TableLayout({ - store, + // store, + store: this.store, table: this, fit: this.fit, showHeader: this.showHeader }); return { layout, - store, + // store, isHidden: false, renderExpanded: null, resizeProxyVisible: false, diff --git a/packages/table/src/util.js b/packages/table/src/util.js index 2125e6c5321..9b155d3944c 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -120,3 +120,135 @@ export const getRowIdentity = (row, rowKey) => { return rowKey.call(null, row); } }; + +export const getKeysMap = function(array, rowKey) { + const arrayMap = {}; + (array || []).forEach((row, index) => { + arrayMap[getRowIdentity(row, rowKey)] = { row, index }; + }); + return arrayMap; +}; + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +export function mergeOptions(defaults, config) { + const options = {}; + let key; + for (key in defaults) { + options[key] = defaults[key]; + } + for (key in config) { + if (hasOwn(config, key)) { + const value = config[key]; + if (typeof value !== 'undefined') { + options[key] = value; + } + } + } + return options; +} + +export function parseWidth(width) { + if (width !== undefined) { + width = parseInt(width, 10); + if (isNaN(width)) { + width = null; + } + } + return width; +}; + +export function parseMinWidth(minWidth) { + if (typeof minWidth !== 'undefined') { + minWidth = parseWidth(minWidth); + if (isNaN(minWidth)) { + minWidth = 80; + } + } + return minWidth; +}; + +// https://github.com/reduxjs/redux/blob/master/src/compose.js +export function compose(...funcs) { + if (funcs.length === 0) { + return arg => arg; + } + if (funcs.length === 1) { + return funcs[0]; + } + return funcs.reduce((a, b) => (...args) => a(b(...args))); +} + +export function toggleRowStatus(statusArr, row, newVal) { + let changed = false; + const index = statusArr.indexOf(row); + const included = index !== -1; + + const addRow = () => { + statusArr.push(row); + changed = true; + }; + const removeRow = () => { + statusArr.splice(index, 1); + changed = true; + }; + + if (typeof newVal === 'boolean') { + if (newVal && !included) { + addRow(); + } else if (!newVal && included) { + removeRow(); + } + } else { + if (included) { + removeRow(); + } else { + addRow(); + } + } + return changed; +} + +export function traverse(root, cb, childrenKey = 'children') { + const isNil = (array) => !(Array.isArray(array) && array.length); + if (isNil(root)) return; + + function _walker(parent, level) { + parent.forEach(node => { + cb(node, parent, level); + if (!isNil(node[childrenKey])) { + _walker(node[childrenKey], level + 1); + } + }); + } + + root.forEach(item => { + const parent = item[childrenKey]; + if (!isNil(parent)) { + _walker(parent, 1); + } + }); +} + +export function walkTreeNode(root, cb, childrenKey = 'children') { + const isNil = (array) => !(Array.isArray(array) && array.length); + + function _walker(parent, children, level) { + cb(parent, children, level); + children.forEach(item => { + const children = item[childrenKey]; + if (!isNil(children)) { + _walker(item, children, level + 1); + } + }); + } + + root.forEach(item => { + const children = item[childrenKey]; + if (!isNil(children)) { + _walker(item, children, 1); + } + }); +} From ed58d09d93514133841578bee4ad6b2dc1e37254 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Wed, 22 May 2019 15:24:56 +0800 Subject: [PATCH 02/18] use helper function --- packages/table/src/store/helper.js | 41 ++++++++++++++++++++++++++ packages/table/src/store/index.js | 28 +++--------------- packages/table/src/table-body.js | 46 +++++++---------------------- packages/table/src/table-footer.js | 41 ++++++++------------------ packages/table/src/table-header.js | 47 +++++++++--------------------- packages/table/src/table.vue | 32 +++++++------------- packages/table/src/util.js | 21 ------------- 7 files changed, 92 insertions(+), 164 deletions(-) create mode 100644 packages/table/src/store/helper.js diff --git a/packages/table/src/store/helper.js b/packages/table/src/store/helper.js new file mode 100644 index 00000000000..5c7aa9dc2cf --- /dev/null +++ b/packages/table/src/store/helper.js @@ -0,0 +1,41 @@ +import Store from './index'; + +export function createStore(table, initialState = {}) { + if (!table) { + throw new Error('Table is required.'); + } + + const store = new Store(); + store.table = table; + Object.keys(initialState).forEach(key => { + store.states[key] = initialState[key]; + }); + // 为 modifiers 中的函数绑定一下 this + Object.keys(store.modifiers).forEach(key => { + store.modifiers[key] = store.modifiers[key].bind(store); + }); + return store; +} + +export function mapStates(mapper) { + const res = {}; + Object.keys(mapper).forEach(key => { + const value = mapper[key]; + let fn; + if (typeof value === 'string') { + fn = function() { + return this.store.states[value]; + }; + } else if (typeof value === 'function') { + fn = function() { + return value.call(this, this.store.states); + }; + } else { + console.error('unexpected value type'); + } + if (fn) { + res[key] = fn; + } + }); + return res; +}; diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js index 80cb5356934..c7016e116a2 100644 --- a/packages/table/src/store/index.js +++ b/packages/table/src/store/index.js @@ -8,11 +8,10 @@ Wachter.prototype.mutations = { states._data = data; this.execQuery(); - // currentRow 更新 + // 数据变化,更新部分数据。 + // 没有使用 computed,而是手动更新部分数据 https://github.com/vuejs/vue/issues/6660#issuecomment-331417140 this.updateCurrentRow(); - // expandRows 更新 this.updateExpandRows(); - // 选择 if (!states.reserveSelection) { if (dataInstanceChanged) { this.clearSelection(); @@ -145,7 +144,7 @@ Wachter.prototype.commit = function(name, ...args) { } }; -// 这样做是否 +// 额外的 DOM 操作都放在 modifiers 中 Wachter.prototype.modifiers = { updateScrollY() { Vue.nextTick(() => { @@ -158,23 +157,4 @@ Wachter.prototype.modifiers = { } }; -export function createStore(table, initialState = {}) { - if (!table) { - throw new Error('Table is required.'); - } - - const store = new Wachter(); - - store.table = table; - Object.keys(initialState).forEach(key => { - store.states[key] = initialState[key]; - }); - - // 绑定一下 this - Object.keys(store.modifiers).forEach(key => { - store.modifiers[key] = store.modifiers[key].bind(store); - }); - - return store; -} - +export default Wachter; diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 6a7d113d3af..d18090db2a9 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -4,6 +4,7 @@ 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', @@ -92,41 +93,16 @@ export default { return this.$parent; }, - data() { - return this.store.states.data; - }, - - // treeData() { - // return this.store.states.treeData; - // }, - - columnsCount() { - return this.store.states.columns.length; - }, - - leftFixedLeafCount() { - return this.store.states.fixedLeafColumnsLength; - }, - - rightFixedLeafCount() { - return this.store.states.rightFixedLeafColumnsLength; - }, - - leftFixedCount() { - return this.store.states.fixedColumns.length; - }, - - rightFixedCount() { - return this.store.states.rightFixedColumns.length; - }, - - columns() { - return this.store.states.columns; - }, - - hasExpandColumn() { - return this.columns.some(({ type }) => type === 'expand'); - } + ...mapStates({ + data: 'data', + columns: 'columns', + 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() { // for (let index = 0; index < this.columns.length; index++) { diff --git a/packages/table/src/table-footer.js b/packages/table/src/table-footer.js index 202555eb863..6ae9c5edd3f 100644 --- a/packages/table/src/table-footer.js +++ b/packages/table/src/table-footer.js @@ -1,4 +1,5 @@ import LayoutObserver from './layout-observer'; +import { mapStates } from './store/helper'; export default { name: 'ElTableFooter', @@ -104,37 +105,19 @@ export default { return this.$parent; }, - isAllSelected() { - return this.store.states.isAllSelected; - }, - - columnsCount() { - return this.store.states.columns.length; - }, - - leftFixedCount() { - return this.store.states.fixedColumns.length; - }, - - leftFixedLeafCount() { - return this.store.states.fixedLeafColumnsLength; - }, - - rightFixedLeafCount() { - return this.store.states.rightFixedLeafColumnsLength; - }, - - rightFixedCount() { - return this.store.states.rightFixedColumns.length; - }, - - columns() { - return this.store.states.columns; - }, - hasGutter() { return !this.fixed && this.tableLayout.gutterWidth; - } + }, + + ...mapStates({ + columns: 'columns', + isAllSelected: 'isAllSelected', + leftFixedLeafCount: 'fixedLeafColumnsLength', + rightFixedLeafCount: 'rightFixedLeafColumnsLength', + columnsCount: states => states.columns.length, + leftFixedCount: states => states.fixedColumns.length, + rightFixedCount: states => states.rightFixedColumns.length + }) }, methods: { diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index 3b718a42ea9..cc4c1a54b7e 100644 --- a/packages/table/src/table-header.js +++ b/packages/table/src/table-header.js @@ -1,9 +1,9 @@ +import Vue from 'vue'; import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom'; import ElCheckbox from 'element-ui/packages/checkbox'; -import ElTag from 'element-ui/packages/tag'; -import Vue from 'vue'; import FilterPanel from './filter-panel.vue'; import LayoutObserver from './layout-observer'; +import { mapStates } from './store/helper'; const getAllColumns = (columns) => { const result = []; @@ -160,8 +160,7 @@ export default { }, components: { - ElCheckbox, - ElTag + ElCheckbox }, computed: { @@ -169,37 +168,19 @@ export default { return this.$parent; }, - isAllSelected() { - return this.store.states.isAllSelected; - }, - - columnsCount() { - return this.store.states.columns.length; - }, - - leftFixedCount() { - return this.store.states.fixedColumns.length; - }, - - rightFixedCount() { - return this.store.states.rightFixedColumns.length; - }, - - leftFixedLeafCount() { - return this.store.states.fixedLeafColumnsLength; - }, - - rightFixedLeafCount() { - return this.store.states.rightFixedLeafColumnsLength; - }, - - columns() { - return this.store.states.columns; - }, - hasGutter() { return !this.fixed && this.tableLayout.gutterWidth; - } + }, + + ...mapStates({ + columns: 'columns', + isAllSelected: 'isAllSelected', + leftFixedLeafCount: 'fixedLeafColumnsLength', + rightFixedLeafCount: 'rightFixedLeafColumnsLength', + columnsCount: states => states.columns.length, + leftFixedCount: states => states.fixedColumns.length, + rightFixedCount: states => states.rightFixedColumns.length + }) }, created() { diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index 25024af169c..70b7099240e 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -219,7 +219,7 @@ import Mousewheel from 'element-ui/src/directives/mousewheel'; import Locale from 'element-ui/src/mixins/locale'; import Migrating from 'element-ui/src/mixins/migrating'; - import { createStore } from './store/index'; + import { createStore, mapStates } from './store/helper'; import TableLayout from './table-layout'; import TableBody from './table-body'; import TableHeader from './table-header'; @@ -480,26 +480,6 @@ this.rightFixedColumns.length > 0; }, - selection() { - return this.store.states.selection; - }, - - columns() { - return this.store.states.columns; - }, - - tableData() { - return this.store.states.data; - }, - - fixedColumns() { - return this.store.states.fixedColumns; - }, - - rightFixedColumns() { - return this.store.states.rightFixedColumns; - }, - bodyWidth() { const { bodyWidth, scrollY, gutterWidth } = this.layout; return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : ''; @@ -560,7 +540,15 @@ height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : '' }; } - } + }, + + ...mapStates({ + selection: 'selection', + columns: 'columns', + tableData: 'data', + fixedColumns: 'fixedColumns', + rightFixedColumns: 'rightFixedColumns' + }) }, watch: { diff --git a/packages/table/src/util.js b/packages/table/src/util.js index 9b155d3944c..e2ec92925da 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -211,27 +211,6 @@ export function toggleRowStatus(statusArr, row, newVal) { return changed; } -export function traverse(root, cb, childrenKey = 'children') { - const isNil = (array) => !(Array.isArray(array) && array.length); - if (isNil(root)) return; - - function _walker(parent, level) { - parent.forEach(node => { - cb(node, parent, level); - if (!isNil(node[childrenKey])) { - _walker(node[childrenKey], level + 1); - } - }); - } - - root.forEach(item => { - const parent = item[childrenKey]; - if (!isNil(parent)) { - _walker(parent, 1); - } - }); -} - export function walkTreeNode(root, cb, childrenKey = 'children') { const isNil = (array) => !(Array.isArray(array) && array.length); From 1462dee408914deb0f87eaa02a5c3ddfc57a5151 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Wed, 22 May 2019 16:12:27 +0800 Subject: [PATCH 03/18] update code style --- packages/table/src/store/expand.js | 2 +- packages/table/src/table-body.js | 2 +- packages/table/src/table-footer.js | 25 ++++++++++++------------- packages/table/src/table-header.js | 28 ++++++++++++++++------------ 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/table/src/store/expand.js b/packages/table/src/store/expand.js index d90996e7bb7..65b7ce8a736 100644 --- a/packages/table/src/store/expand.js +++ b/packages/table/src/store/expand.js @@ -34,7 +34,7 @@ export default { toggleRowExpansion(row, expanded) { const changed = toggleRowStatus(this.states.expandRows, row, expanded); if (changed) { - this.table.$emit('expand-change', row, this.states.expandRows); + this.table.$emit('expand-change', row, this.states.expandRows.slice()); this.scheduleLayout(); } }, diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index d18090db2a9..2de5b7fad2f 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -61,7 +61,7 @@ export default { border="0"> { - this._l(this.columns, column => ) + this.columns.map(column => ) } diff --git a/packages/table/src/table-footer.js b/packages/table/src/table-footer.js index 6ae9c5edd3f..8d51c97da89 100644 --- a/packages/table/src/table-footer.js +++ b/packages/table/src/table-footer.js @@ -50,7 +50,7 @@ export default { border="0"> { - this._l(this.columns, column => ) + this.columns.map(column => ) } { this.hasGutter ? : '' @@ -59,18 +59,17 @@ export default { { - this._l(this.columns, (column, cellIndex) => - -
- { - sums[cellIndex] - } -
- - ) + this.columns.map((column, cellIndex) => +
+ { + sums[cellIndex] + } +
+ ) } { this.hasGutter ? : '' diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index cc4c1a54b7e..04bcd451e5e 100644 --- a/packages/table/src/table-header.js +++ b/packages/table/src/table-header.js @@ -82,7 +82,7 @@ export default { border="0"> { - this._l(this.columns, column => ) + this.columns.map(column => ) } { this.hasGutter ? : '' @@ -114,19 +114,23 @@ export default { : column.label } { - column.sortable - ? this.handleSortClick($event, column) }> - this.handleSortClick($event, column, 'ascending') }> - - this.handleSortClick($event, column, 'descending') }> - - - : '' + column.sortable ? ( this.handleSortClick($event, column) }> + this.handleSortClick($event, column, 'ascending') }> + + this.handleSortClick($event, column, 'descending') }> + + ) : '' } { - column.filterable - ? this.handleFilterClick($event, column) }> - : '' + column.filterable ? ( this.handleFilterClick($event, column) }> + + ) : '' } )) From b57ce6e0e9388ca67e932095bec0a482ffa21539 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Wed, 22 May 2019 19:25:32 +0800 Subject: [PATCH 04/18] add tree-props --- packages/table/src/config.js | 19 ++++++ packages/table/src/store/index.js | 2 +- packages/table/src/store/tree.js | 102 +++++++++++++++++++++++----- packages/table/src/store/watcher.js | 3 +- packages/table/src/table-body.js | 2 +- packages/table/src/table-column.js | 31 +-------- packages/table/src/table.vue | 10 +++ packages/table/src/util.js | 2 +- 8 files changed, 122 insertions(+), 49 deletions(-) diff --git a/packages/table/src/config.js b/packages/table/src/config.js index 9b694a7b1b7..d76b94bfcbe 100644 --- a/packages/table/src/config.js +++ b/packages/table/src/config.js @@ -97,3 +97,22 @@ export function defaultRenderCell(h, { row, column, $index }) { } return value; } + +export function treeCellPrefix(h, { row, treeNode, store }) { + if (!treeNode) return null; + const ele = []; + const callback = function(e) { + e.stopPropagation(); + store.toggleTreeExpansion(row); + }; + ele.push(); + if (treeNode.hasChildren) { + ele.push(
+ +
); + } else { + ele.push(); + } + return ele; +} diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js index c7016e116a2..cd2309cc9f8 100644 --- a/packages/table/src/store/index.js +++ b/packages/table/src/store/index.js @@ -19,7 +19,7 @@ Wachter.prototype.mutations = { this.cleanSelection(); } } else { - this.assertRowkey(); + this.assertRowKey(); this.updateSelectionByRowKey(); } this.updateAllSelected(); diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js index d339d192cec..3cb5c05ed43 100644 --- a/packages/table/src/store/tree.js +++ b/packages/table/src/store/tree.js @@ -1,44 +1,112 @@ -// import { getKeysMap, getRowIdentity, walkTreeNode } from '../util'; +import { walkTreeNode, getRowIdentity } from '../util'; export default { data() { return { states: { // defaultExpandAll 存在于 expand.js 中,在这里只是注释掉。 - // TODO:拆分为独立的 TreeTale // defaultExpandAll: false, + // TODO: 拆分为独立的 TreeTale + // TODO: 在 expand 中,展开行的记录是放在 expandRows 中,统一用法 + expandRowKeys: [], treeData: {}, indent: 16, lazy: false, lazyTreeNodeMap: {}, + lazyColumnIndentifier: 'hasChildren', childrenColumnName: 'children' } }; }, - methods: { - updateTreeData() { - // 这里要处理一下数据合并的情形 - this.assertRowKey(); - - // const { states, table } = this; - // const { data = [], rowKey } = states; - // const newTreeData = {}; - // const oldTreeData = states.treeData; - // walkTreeNode(data, (parent, children, level) => { - // }, this.childrenColumnName); - }, + computed: { + // 这里的计算属性是私有的 + // 嵌入型的数据,watch 无法是检测到变化 https://github.com/ElemeFE/element/issues/14998 + // TODO: 使用 computed 解决该问题,是否会造成性能问题? + simpleNestedData() { + const { childrenColumnName, rowKey } = this.states; + if (!rowKey) { + return {}; + } + const data = this.states.data || []; + const res = {}; + // Vue 对 computed 做了一层 try...catch,在这里不调用 assertRowKey 方法 + let invalid = false; + const assert = id => { + if (id === null || id === undefined) { + invalid = true; + } + }; + walkTreeNode(data, (parent, children, level) => { + if (invalid) return; + const parentId = getRowIdentity(parent, rowKey); + assert(parentId); + const childrenIdArr = []; + children.forEach(row => { + const id = getRowIdentity(row, rowKey); + assert(id); + childrenIdArr.push(id); + }); + res[parentId] = { + children: childrenIdArr, + level + }; + }, childrenColumnName); + if (invalid) { + console.warn('[Element Warn]for nested data, rowKey of every row should be specified.'); + return {}; + } else { + return res; + } + } + }, - updateTreeExpansion() { + watch: { + simpleNestedData: 'updateTreeData', + // 当 expandRowKeys 发生变化时,也许要更新 treeData + expandRowKeys: 'updateTreeData' + }, + methods: { + updateTreeData() { + const nested = this.simpleNestedData; + const keys = Object.keys(nested); + if (!keys.length) return; + const { treeData: oldTreeData, defaultExpandAll, expandRowKeys } = this.states; + const newTreeData = {}; + // 合并 expanded 与 display,确保数据刷新后,状态不变 + keys.forEach(key => { + const newValue = { ...nested[key] }; + const oldValue = oldTreeData[key]; + if (newValue.children) { + // 这里 children 不可能为空数组的 + if (newValue.children.length === 0) { + throw new Error('children should not be an empty array.'); + } + const included = defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1); + newValue.expanded = (oldValue && oldValue.expanded) || included; + } + // const isParentExpaned = + // newValue.display = (newValue.level === 0) || (oldValue && oldValue.display) ||; + newTreeData[key] = newValue; + }); + this.states.treeData = newTreeData; }, - toggleTreeExpansion() { + toggleTreeExpansion(row, expanded) { + this.assertRowKey(); + const { rowKey, treeData } = this.states; + const id = getRowIdentity(row, rowKey); + const data = id && treeData[id]; + if (id && data && ('expanded' in data)) { + expanded = typeof expanded !== 'undefined' ? !data.expanded : expanded; + treeData[id].expanded = expanded; + } }, loadData() { - + console.log('todo'); } } }; diff --git a/packages/table/src/store/watcher.js b/packages/table/src/store/watcher.js index d4171359659..c84e6941f1e 100644 --- a/packages/table/src/store/watcher.js +++ b/packages/table/src/store/watcher.js @@ -4,6 +4,7 @@ import merge from 'element-ui/src/utils/merge'; import { getKeysMap, getRowIdentity, getColumnById, getColumnByKey, orderBy, toggleRowStatus } from '../util'; import expand from './expand'; import current from './current'; +import tree from './tree'; // import const sortData = (data, states) => { @@ -74,7 +75,7 @@ export default Vue.extend({ }; }, - mixins: [expand, current], + mixins: [expand, current, tree], methods: { // 检查 rowKey 是否存在 diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 2de5b7fad2f..52e00c59d08 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -360,7 +360,7 @@ export default { on-click={ ($event) => this.handleClick($event, row) } on-contextmenu={ ($event) => this.handleContextMenu($event, row) } on-mouseenter={ _ => this.handleMouseEnter($index) } - on-mouseleave={ _ => this.handleMouseLeave() } + on-mouseleave={ this.handleMouseLeave } class={ [this.getRowClass(row, $index)] }> { this.columns.map((column, cellIndex) => { diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js index 626daf7002e..9efb7b63bcd 100644 --- a/packages/table/src/table-column.js +++ b/packages/table/src/table-column.js @@ -1,4 +1,4 @@ -import { cellStarts, cellForced, defaultRenderCell } from './config'; +import { cellStarts, cellForced, defaultRenderCell, treeCellPrefix } from './config'; import { mergeOptions, parseWidth, parseMinWidth, compose } from './util'; import ElCheckbox from 'element-ui/packages/checkbox'; @@ -177,6 +177,7 @@ export default { } else { children = originRenderCell(h, data); } + const prefix = treeCellPrefix(h, data); const props = { class: 'cell', style: {} @@ -186,7 +187,7 @@ export default { props.style = {width: (data.column.realWidth || data.column.width) - 1 + 'px'}; } return (
- { this.renderTreeCell(data) } + { prefix } { children }
); }; @@ -237,32 +238,6 @@ export default { this.owner.store.scheduleLayout(updateColumns); }); }); - }, - - // TODO: 移除这里的实现 - renderTreeCell(data) { - if (!data.treeNode) return null; - console.warn('tree 的相关逻辑要调整'); - const ele = []; - ele.push(); - if (data.treeNode.hasChildren) { - ele.push(
- -
); - } else { - ele.push(); - } - return ele; - }, - - handleTreeExpandIconClick(data, e) { - e.stopPropagation(); - if (data.store.states.lazy && !data.treeNode.loaded) { - data.store.loadData(data.row, data.treeNode); - } else { - data.store.toggleTreeExpansion(data.treeNode.rowKey); - } } }, diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index 70b7099240e..79cd5d7f455 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -318,6 +318,16 @@ default: 16 }, + treeProps: { + type: Object, + default() { + return { + hasChildren: 'hasChildren', + children: 'children' + }; + } + }, + lazy: Boolean, load: Function diff --git a/packages/table/src/util.js b/packages/table/src/util.js index e2ec92925da..33a46dd08eb 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -227,7 +227,7 @@ export function walkTreeNode(root, cb, childrenKey = 'children') { root.forEach(item => { const children = item[childrenKey]; if (!isNil(children)) { - _walker(item, children, 1); + _walker(item, children, 0); } }); } From 84c21bdaf65e74b606a59b139b9ec6241734b80b Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Thu, 23 May 2019 19:07:56 +0800 Subject: [PATCH 05/18] TreeTable render well --- packages/table/src/config.js | 2 +- packages/table/src/store/tree.js | 12 +- packages/table/src/store/watcher.js | 7 + packages/table/src/table-body.js | 214 ++++++++++++++++++---------- packages/table/src/table.vue | 16 +-- 5 files changed, 158 insertions(+), 93 deletions(-) diff --git a/packages/table/src/config.js b/packages/table/src/config.js index d76b94bfcbe..0e688910565 100644 --- a/packages/table/src/config.js +++ b/packages/table/src/config.js @@ -106,7 +106,7 @@ export function treeCellPrefix(h, { row, treeNode, store }) { store.toggleTreeExpansion(row); }; ele.push(); - if (treeNode.hasChildren) { + if (typeof treeNode.expanded === 'boolean') { ele.push(
diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js index 3cb5c05ed43..ac842862974 100644 --- a/packages/table/src/store/tree.js +++ b/packages/table/src/store/tree.js @@ -86,8 +86,6 @@ export default { const included = defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1); newValue.expanded = (oldValue && oldValue.expanded) || included; } - // const isParentExpaned = - // newValue.display = (newValue.level === 0) || (oldValue && oldValue.display) ||; newTreeData[key] = newValue; }); this.states.treeData = newTreeData; @@ -100,11 +98,19 @@ export default { const id = getRowIdentity(row, rowKey); const data = id && treeData[id]; if (id && data && ('expanded' in data)) { - expanded = typeof expanded !== 'undefined' ? !data.expanded : expanded; + expanded = typeof expanded === 'undefined' ? !data.expanded : expanded; treeData[id].expanded = expanded; } }, + updateTreeExpandKeys(value) { + // 仅仅在包含嵌套数据时才去更新 + if (Object.keys(this.simpleNestedData).length) { + this.states.expandRowKeys = value; + this.updateTreeData(); + } + }, + loadData() { console.log('todo'); } diff --git a/packages/table/src/store/watcher.js b/packages/table/src/store/watcher.js index c84e6941f1e..9e811c8ccc0 100644 --- a/packages/table/src/store/watcher.js +++ b/packages/table/src/store/watcher.js @@ -367,6 +367,13 @@ export default Vue.extend({ this.commit('changeSortCondition', { silent: true }); + }, + + // 适配层,expand-row-keys 在 Expand 与 TreeTable 中都有使用 + setExpandRowKeysAdpter(val) { + // 这里会触发额外的计算,但为了兼容性,暂时这么做 + this.setExpandRowKeys(val); + this.updateTreeExpandKeys(val); } } }); diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 52e00c59d08..267ea2ef5b4 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -1,3 +1,4 @@ +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'; @@ -30,29 +31,6 @@ export default { render(h) { const data = this.data || []; - // let rows = this.data; - // if (this.store.states.lazy && Object.keys(this.store.states.lazyTreeNodeMap).length) { - // rows = rows.reduce((prev, item) => { - // prev.push(item); - // const rowKey = this.store.table.getRowKey(item); - // const parent = this.store.states.treeData[rowKey]; - // if (parent && parent.children && parent.hasChildren) { - // const tmp = []; - // const traverse = (children) => { - // if (!children) return; - // children.forEach(key => { - // tmp.push(this.store.states.lazyTreeNodeMap[key]); - // if (this.store.states.treeData[key]) { - // traverse(this.store.states.treeData[key].children); - // } - // }); - // }; - // traverse(parent.children); - // prev = prev.concat(tmp); - // } - // return prev; - // }, []); - // } return ( { - data.map((row, $index) => { - const tr = this.rowRender(row, $index); - if (this.hasExpandColumn && this.store.isRowExpanded(row)) { - return [ - tr, - - - - ]; - } else { - return tr; - } - }) + data.reduce((acc, row) => { + return acc.concat(this.wrappedRowRender(row, acc.length)); + }, []) } @@ -96,32 +62,25 @@ export default { ...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() { - // for (let index = 0; index < this.columns.length; index++) { - // if (this.columns[index].type === 'default') { - // return index; - // } - // } - // return 0; - // }, - - // treeIndent() { - // return this.store.states.indent; - // } + }), + + 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) { + console.log('value is ', newVal); if (!this.store.states.isComplex) return; const rows = this.$el.querySelectorAll('.el-table__row'); const oldRow = rows[oldVal]; @@ -167,7 +126,6 @@ export default { getSpan(row, column, rowIndex, columnIndex) { let rowspan = 1; let colspan = 1; - const fn = this.table.spanMethod; if (typeof fn === 'function') { const result = fn({ @@ -176,7 +134,6 @@ export default { rowIndex, columnIndex }); - if (Array.isArray(result)) { rowspan = result[0]; colspan = result[1]; @@ -185,11 +142,7 @@ export default { colspan = result.colspan; } } - - return { - rowspan, - colspan - }; + return { rowspan, colspan }; }, getRowStyle(row, rowIndex) { @@ -200,7 +153,7 @@ export default { rowIndex }); } - return rowStyle; + return rowStyle || null; }, getRowClass(row, rowIndex) { @@ -264,6 +217,14 @@ export default { 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); @@ -346,28 +307,51 @@ export default { table.$emit(`row-${name}`, row, column, event); }, - handleExpandClick(row, e) { - e.stopPropagation(); - this.store.toggleRowExpansion(row); - }, - - rowRender(row, $index) { - const columnsHidden = this.columns.map((column, index) => this.isColumnHidden(index)); + 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; + } return ( 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 } - class={ [this.getRowClass(row, $index)] }> + on-mouseleave={ this.handleMouseLeave }> { - this.columns.map((column, cellIndex) => { + 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 + // loaded: treeNode.loaded + }; + if (typeof treeRowData.expanded === 'boolean') { + data.treeNode.expanded = treeRowData.expanded; + } + console.log('data treeNode ', data.treeNode); + } return ( + + + ]; + } 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 + }; + } + const tmp = [this.rowRender(row, $index, treeRowData)]; + // 渲染嵌套数据 + if (cur) { + // 对 TreeTable 中的 row 的 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 + // loaded: false + }; + const childKey = getRowIdentity(node, rowKey); + if (typeof childKey === 'undefined') { + throw new Error('for nested data item, row-key is required.'); + } + cur = treeData[childKey]; + // 对于当前节点,分成有无子节点两种情况。 + // 如果包含子节点的,设置 expanded 属性。 + // 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定 + if (cur) { + innerTreeRowData.expanded = cur.expanded; + cur.display = cur.expanded && innerTreeRowData.display; + } + i++; + tmp.push(this.rowRender(node, $index + i, innerTreeRowData)); + if (cur) { + traverse(node[childrenColumnName], cur); + } + }); + }; + // 对于 root 节点,display 为 true + cur.display = true; + traverse(row[childrenColumnName], cur); + } + return tmp; + } else { + return this.rowRender(row, $index); + } } } }; diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index 79cd5d7f455..e29ffd5e0a7 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -596,7 +596,7 @@ immediate: true, handler(newVal) { if (newVal) { - this.store.setExpandRowKeys(newVal); + this.store.setExpandRowKeysAdpter(newVal); } } } @@ -636,20 +636,17 @@ }, data() { + const { hasChildren = 'hasChildren', children = 'children' } = this.treeProps; this.store = createStore(this, { rowKey: this.rowKey, defaultExpandAll: this.defaultExpandAll, selectOnIndeterminate: this.selectOnIndeterminate, + // TreeTable 的相关配置 indent: this.indent, - lazy: this.lazy + lazy: this.lazy, + lazyColumnIndentifier: hasChildren, + childrenColumnName: children }); - // const store = new TableStore(this, { - // rowKey: this.rowKey, - // defaultExpandAll: this.defaultExpandAll, - // selectOnIndeterminate: this.selectOnIndeterminate, - // indent: this.indent, - // lazy: this.lazy - // }); const layout = new TableLayout({ // store, store: this.store, @@ -659,7 +656,6 @@ }); return { layout, - // store, isHidden: false, renderExpanded: null, resizeProxyVisible: false, From 0cb3b5187057a9b2f24555dc3173eed43fc183f3 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Thu, 23 May 2019 20:28:57 +0800 Subject: [PATCH 06/18] support lazy loading --- packages/table/src/config.js | 2 +- packages/table/src/store/tree.js | 46 +++++++++++++++++++++-------- packages/table/src/store/watcher.js | 12 +++++++- packages/table/src/table-body.js | 1 - packages/table/src/table.vue | 4 +-- packages/table/src/util.js | 10 ++++++- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/table/src/config.js b/packages/table/src/config.js index 0e688910565..47226e06df2 100644 --- a/packages/table/src/config.js +++ b/packages/table/src/config.js @@ -103,7 +103,7 @@ export function treeCellPrefix(h, { row, treeNode, store }) { const ele = []; const callback = function(e) { e.stopPropagation(); - store.toggleTreeExpansion(row); + store.loadOrToggle(row); }; ele.push(); if (typeof treeNode.expanded === 'boolean') { diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js index ac842862974..99388154bb9 100644 --- a/packages/table/src/store/tree.js +++ b/packages/table/src/store/tree.js @@ -24,7 +24,7 @@ export default { // 嵌入型的数据,watch 无法是检测到变化 https://github.com/ElemeFE/element/issues/14998 // TODO: 使用 computed 解决该问题,是否会造成性能问题? simpleNestedData() { - const { childrenColumnName, rowKey } = this.states; + const { childrenColumnName, lazyColumnIndentifier, rowKey, lazy } = this.states; if (!rowKey) { return {}; } @@ -51,7 +51,15 @@ export default { children: childrenIdArr, level }; - }, childrenColumnName); + // 当 childrenIdArr 为空时标明该 row 需要加载数据 + if (lazy) { + res[parentId] = { + ...res[parentId], + loaded: false, + loading: false + }; + } + }, childrenColumnName, lazyColumnIndentifier); if (invalid) { console.warn('[Element Warn]for nested data, rowKey of every row should be specified.'); return {}; @@ -79,10 +87,6 @@ export default { const newValue = { ...nested[key] }; const oldValue = oldTreeData[key]; if (newValue.children) { - // 这里 children 不可能为空数组的 - if (newValue.children.length === 0) { - throw new Error('children should not be an empty array.'); - } const included = defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1); newValue.expanded = (oldValue && oldValue.expanded) || included; } @@ -91,28 +95,44 @@ export default { this.states.treeData = newTreeData; }, + updateTreeExpandKeys(value) { + // 仅仅在包含嵌套数据时才去更新 + if (Object.keys(this.simpleNestedData).length) { + this.states.expandRowKeys = value; + this.updateTreeData(); + } + }, + toggleTreeExpansion(row, expanded) { this.assertRowKey(); const { rowKey, treeData } = this.states; const id = getRowIdentity(row, rowKey); const data = id && treeData[id]; + const oldExpanded = treeData[id].expanded; if (id && data && ('expanded' in data)) { expanded = typeof expanded === 'undefined' ? !data.expanded : expanded; treeData[id].expanded = expanded; + if (oldExpanded !== expanded) { + this.table.$emit('expand-change', row); + } } }, - updateTreeExpandKeys(value) { - // 仅仅在包含嵌套数据时才去更新 - if (Object.keys(this.simpleNestedData).length) { - this.states.expandRowKeys = value; - this.updateTreeData(); + loadOrToggle(row) { + this.assertRowKey(); + const { lazy, treeData, rowKey } = this.states; + const id = getRowIdentity(row, rowKey); + const data = treeData[id]; + if (lazy && data && !data.loaded) { + this.loadData(row, data); + } else { + this.toggleTreeExpansion(row); } }, - loadData() { - console.log('todo'); + loadData(row, loadData) { + console.log('todo ==>'); } } }; diff --git a/packages/table/src/store/watcher.js b/packages/table/src/store/watcher.js index 9e811c8ccc0..16fded778cf 100644 --- a/packages/table/src/store/watcher.js +++ b/packages/table/src/store/watcher.js @@ -370,10 +370,20 @@ export default Vue.extend({ }, // 适配层,expand-row-keys 在 Expand 与 TreeTable 中都有使用 - setExpandRowKeysAdpter(val) { + setExpandRowKeysAdapter(val) { // 这里会触发额外的计算,但为了兼容性,暂时这么做 this.setExpandRowKeys(val); this.updateTreeExpandKeys(val); + }, + + // 展开行与 TreeTable 都要使用 + toggleRowExpansionAdapter(row, expanded) { + const hasExpandColumn = this.states.columns.some(({ type }) => type === 'expand'); + if (hasExpandColumn) { + this.toggleRowExpansion(row, expanded); + } else { + this.toggleTreeRowExpansion(row, expanded); + } } } }); diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 267ea2ef5b4..f24fe8e9922 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -80,7 +80,6 @@ export default { // 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) { - console.log('value is ', newVal); if (!this.store.states.isComplex) return; const rows = this.$el.querySelectorAll('.el-table__row'); const oldRow = rows[oldVal]; diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index e29ffd5e0a7..e1440bf46d2 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -359,7 +359,7 @@ }, toggleRowExpansion(row, expanded) { - this.store.toggleRowExpansion(row, expanded); + this.store.toggleRowExpansionAdapter(row, expanded); }, clearSelection() { @@ -596,7 +596,7 @@ immediate: true, handler(newVal) { if (newVal) { - this.store.setExpandRowKeysAdpter(newVal); + this.store.setExpandRowKeysAdapter(newVal); } } } diff --git a/packages/table/src/util.js b/packages/table/src/util.js index 33a46dd08eb..fbe2a36d760 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -211,12 +211,16 @@ export function toggleRowStatus(statusArr, row, newVal) { return changed; } -export function walkTreeNode(root, cb, childrenKey = 'children') { +export function walkTreeNode(root, cb, childrenKey = 'children', lazyKey = 'hasChildren') { const isNil = (array) => !(Array.isArray(array) && array.length); function _walker(parent, children, level) { cb(parent, children, level); children.forEach(item => { + if (item[lazyKey]) { + cb(item, [], level + 1); + return; + } const children = item[childrenKey]; if (!isNil(children)) { _walker(item, children, level + 1); @@ -225,6 +229,10 @@ export function walkTreeNode(root, cb, childrenKey = 'children') { } root.forEach(item => { + if (item[lazyKey]) { + cb(item, [], 0); + return; + } const children = item[childrenKey]; if (!isNil(children)) { _walker(item, children, 0); From 16f5dc208ccaa222670e7048a8d728351dc7dbe7 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Fri, 24 May 2019 15:58:30 +0800 Subject: [PATCH 07/18] lazy load --- packages/table/src/config.js | 4 +- packages/table/src/store/helper.js | 3 +- packages/table/src/store/index.js | 4 - packages/table/src/store/tree.js | 162 ++++++++++++++++++----------- packages/table/src/table-body.js | 25 +++-- packages/table/src/util.js | 4 +- 6 files changed, 124 insertions(+), 78 deletions(-) diff --git a/packages/table/src/config.js b/packages/table/src/config.js index 47226e06df2..31f674040b8 100644 --- a/packages/table/src/config.js +++ b/packages/table/src/config.js @@ -105,7 +105,9 @@ export function treeCellPrefix(h, { row, treeNode, store }) { e.stopPropagation(); store.loadOrToggle(row); }; - ele.push(); + if (treeNode.indent) { + ele.push(); + } if (typeof treeNode.expanded === 'boolean') { ele.push(
diff --git a/packages/table/src/store/helper.js b/packages/table/src/store/helper.js index 5c7aa9dc2cf..393660c2c8e 100644 --- a/packages/table/src/store/helper.js +++ b/packages/table/src/store/helper.js @@ -10,7 +10,6 @@ export function createStore(table, initialState = {}) { Object.keys(initialState).forEach(key => { store.states[key] = initialState[key]; }); - // 为 modifiers 中的函数绑定一下 this Object.keys(store.modifiers).forEach(key => { store.modifiers[key] = store.modifiers[key].bind(store); }); @@ -31,7 +30,7 @@ export function mapStates(mapper) { return value.call(this, this.store.states); }; } else { - console.error('unexpected value type'); + console.error('invalid value type'); } if (fn) { res[key] = fn; diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js index cd2309cc9f8..dca1f089e24 100644 --- a/packages/table/src/store/index.js +++ b/packages/table/src/store/index.js @@ -150,10 +150,6 @@ Wachter.prototype.modifiers = { Vue.nextTick(() => { this.table.updateScrollY(); }); - }, - - updateCurrentRow() { - console.log('todo ===> '); } }; diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js index 99388154bb9..c83d544b01f 100644 --- a/packages/table/src/store/tree.js +++ b/packages/table/src/store/tree.js @@ -4,10 +4,8 @@ export default { data() { return { states: { - // defaultExpandAll 存在于 expand.js 中,在这里只是注释掉。 - // defaultExpandAll: false, - // TODO: 拆分为独立的 TreeTale - // TODO: 在 expand 中,展开行的记录是放在 expandRows 中,统一用法 + // defaultExpandAll 存在于 expand.js 中,这里不重复添加 + // TODO: 拆分为独立的 TreeTale,在 expand 中,展开行的记录是放在 expandRows 中,统一用法 expandRowKeys: [], treeData: {}, indent: 16, @@ -20,84 +18,119 @@ export default { }, computed: { - // 这里的计算属性是私有的 // 嵌入型的数据,watch 无法是检测到变化 https://github.com/ElemeFE/element/issues/14998 // TODO: 使用 computed 解决该问题,是否会造成性能问题? - simpleNestedData() { - const { childrenColumnName, lazyColumnIndentifier, rowKey, lazy } = this.states; - if (!rowKey) { - return {}; - } + // @return { id: { level, children, loaded? }} + normalizedData() { + if (!this.states.rowKey) return {}; const data = this.states.data || []; + return this.normalize(data); + }, + // @return { id: { level, children, loaded? } } + // 针对懒加载的情形,不处理嵌套数据 + normalizedLazyNode() { + const { rowKey, lazyTreeNodeMap, lazyColumnIndentifier } = this.states; + const keys = Object.keys(lazyTreeNodeMap); const res = {}; - // Vue 对 computed 做了一层 try...catch,在这里不调用 assertRowKey 方法 - let invalid = false; - const assert = id => { - if (id === null || id === undefined) { - invalid = true; - } - }; - walkTreeNode(data, (parent, children, level) => { - if (invalid) return; - const parentId = getRowIdentity(parent, rowKey); - assert(parentId); - const childrenIdArr = []; - children.forEach(row => { - const id = getRowIdentity(row, rowKey); - assert(id); - childrenIdArr.push(id); - }); - res[parentId] = { - children: childrenIdArr, - level - }; - // 当 childrenIdArr 为空时标明该 row 需要加载数据 - if (lazy) { - res[parentId] = { - ...res[parentId], - loaded: false, - loading: false - }; + if (!keys.length) return res; + keys.forEach(key => { + if (lazyTreeNodeMap[key].length) { + const item = { children: [] }; + lazyTreeNodeMap[key].forEach(row => { + const currentRowKey = getRowIdentity(row, rowKey); + item.children.push(currentRowKey); + if (row[lazyColumnIndentifier] && !res[currentRowKey]) { + res[currentRowKey] = { children: [] }; + } + }); + res[key] = item; } - }, childrenColumnName, lazyColumnIndentifier); - if (invalid) { - console.warn('[Element Warn]for nested data, rowKey of every row should be specified.'); - return {}; - } else { - return res; - } + }); + return res; } }, watch: { - simpleNestedData: 'updateTreeData', - // 当 expandRowKeys 发生变化时,也许要更新 treeData - expandRowKeys: 'updateTreeData' + normalizedData: 'updateTreeData', + // expandRowKeys 在 TreeTable 中也有使用 + expandRowKeys: 'updateTreeData', + normalizedLazyNode: 'updateTreeData' }, methods: { + normalize(data) { + const { childrenColumnName, lazyColumnIndentifier, rowKey, lazy } = this.states; + const res = {}; + walkTreeNode(data, (parent, children, level) => { + const parentId = getRowIdentity(parent, rowKey); + if (Array.isArray(children)) { + res[parentId] = { + children: children.map(row => getRowIdentity(row, rowKey)), + level + }; + } else if (lazy) { + // 当 children 不存在且 lazy 为 true,该节点即为懒加载的节点 + res[parentId] = { + children: [], + lazy: true, + level + }; + } + }, childrenColumnName, lazyColumnIndentifier); + return res; + }, + updateTreeData() { - const nested = this.simpleNestedData; + const nested = this.normalizedData; + const normalizedLazyNode = this.normalizedLazyNode; const keys = Object.keys(nested); if (!keys.length) return; - const { treeData: oldTreeData, defaultExpandAll, expandRowKeys } = this.states; + const { treeData: oldTreeData, defaultExpandAll, expandRowKeys, lazy } = this.states; const newTreeData = {}; + const rootLazyRowKeys = []; + const getExpanded = (oldValue, key) => { + const included = defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1); + return !!((oldValue && oldValue.expanded) || included); + }; // 合并 expanded 与 display,确保数据刷新后,状态不变 keys.forEach(key => { - const newValue = { ...nested[key] }; const oldValue = oldTreeData[key]; - if (newValue.children) { - const included = defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1); - newValue.expanded = (oldValue && oldValue.expanded) || included; + const newValue = { ...nested[key] }; + newValue.expanded = getExpanded(oldValue, key); + if (newValue.lazy) { + newValue.loaded = !!(oldValue && oldValue.loaded); + rootLazyRowKeys.push(key); } newTreeData[key] = newValue; }); + // 根据懒加载数据更新 treeData + const lazyKeys = Object.keys(normalizedLazyNode); + if (lazy && lazyKeys.length && rootLazyRowKeys.length) { + lazyKeys.forEach(key => { + const oldValue = oldTreeData[key]; + const lazyNodeChildren = normalizedLazyNode[key].children; + if (rootLazyRowKeys.indexOf(key) !== -1) { + // 懒加载的 root 节点,更新一下原有的数据,原来的 children 一定是空数组 + if (newTreeData[key].children.length !== 0) { + throw new Error('[ElTable]children must be an empty array.'); + } + newTreeData[key].children = lazyNodeChildren; + } else { + newTreeData[key] = { + loaded: !!(oldValue && oldValue.loaded), + expanded: getExpanded(oldValue, key), + children: lazyNodeChildren, + level: '' + }; + } + }); + } this.states.treeData = newTreeData; }, updateTreeExpandKeys(value) { // 仅仅在包含嵌套数据时才去更新 - if (Object.keys(this.simpleNestedData).length) { + if (Object.keys(this.normalizedData).length) { this.states.expandRowKeys = value; this.updateTreeData(); } @@ -125,14 +158,27 @@ export default { const id = getRowIdentity(row, rowKey); const data = treeData[id]; if (lazy && data && !data.loaded) { - this.loadData(row, data); + this.loadData(row, id, data); } else { this.toggleTreeExpansion(row); } }, - loadData(row, loadData) { - console.log('todo ==>'); + loadData(row, key, treeNode) { + const { load } = this.table; + const { lazyTreeNodeMap, treeData } = this.states; + if (load && !treeData[key].loaded) { + load(row, treeNode, (data) => { + if (!Array.isArray(data)) { + throw new Error('[ElTable] data must be an array'); + } + treeData[key].loaded = true; + treeData[key].expanded = true; + if (data.length) { + this.$set(lazyTreeNodeMap, key, data); + } + }); + } } } }; diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index f24fe8e9922..7d3c5641970 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -349,7 +349,7 @@ export default { if (typeof treeRowData.expanded === 'boolean') { data.treeNode.expanded = treeRowData.expanded; } - console.log('data treeNode ', data.treeNode); + // console.log('data treeNode ', data.treeNode); } return (
- { this.table.renderExpanded ? this.table.renderExpanded(h, { row, $index, store: this.store }) : ''} -
); + }, + + wrappedRowRender(row, $index) { + const store = this.store; + const { isRowExpanded, assertRowKey } = store; + const { treeData, childrenColumnName, rowKey } = store.states; + if (this.hasExpandColumn && isRowExpanded(row)) { + const renderExpanded = this.table.renderExpanded; + // 因为展开行是成倍增加的,把 index 的数量减半 + $index = $index / 2; + const tr = this.rowRender(row, $index); + if (!renderExpanded) { + console.error('[Element Error]renderExpanded is required.'); + return tr; + } + return [ + tr, +
+ { renderExpanded(this.$createElement, { row, $index, store: this.store }) } +
{ if (!(children && children.length && parent)) return; @@ -421,30 +421,33 @@ export default { const innerTreeRowData = { display: parent.display && parent.expanded, level: parent.level + 1 - // loaded: false }; const childKey = getRowIdentity(node, rowKey); - if (typeof childKey === 'undefined') { + if (childKey === undefined || childKey === null) { throw new Error('for nested data item, row-key is required.'); } - cur = treeData[childKey]; + cur = { ...treeData[childKey] }; // 对于当前节点,分成有无子节点两种情况。 // 如果包含子节点的,设置 expanded 属性。 - // 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定 + // 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定。 if (cur) { innerTreeRowData.expanded = cur.expanded; - cur.display = cur.expanded && innerTreeRowData.display; + // 懒加载的某些节点,level 未知 + cur.level = cur.level || innerTreeRowData.level; + cur.display = !!(cur.expanded && innerTreeRowData.display); } i++; tmp.push(this.rowRender(node, $index + i, innerTreeRowData)); if (cur) { - traverse(node[childrenColumnName], cur); + const nodes = lazyTreeNodeMap[childKey] || node[childrenColumnName]; + traverse(nodes, cur); } }); }; - // 对于 root 节点,display 为 true + // 对于 root 节点,display 一定为 true cur.display = true; - traverse(row[childrenColumnName], cur); + const nodes = lazyTreeNodeMap[key] || row[childrenColumnName]; + traverse(nodes, cur); } return tmp; } else { diff --git a/packages/table/src/util.js b/packages/table/src/util.js index fbe2a36d760..f4171d10cd2 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -218,7 +218,7 @@ export function walkTreeNode(root, cb, childrenKey = 'children', lazyKey = 'hasC cb(parent, children, level); children.forEach(item => { if (item[lazyKey]) { - cb(item, [], level + 1); + cb(item, null, level + 1); return; } const children = item[childrenKey]; @@ -230,7 +230,7 @@ export function walkTreeNode(root, cb, childrenKey = 'children', lazyKey = 'hasC root.forEach(item => { if (item[lazyKey]) { - cb(item, [], 0); + cb(item, null, 0); return; } const children = item[childrenKey]; From a64af87b59c641a7edecea3f54298c8399f9f763 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Tue, 28 May 2019 17:02:14 +0800 Subject: [PATCH 08/18] fix max-height --- packages/table/src/store/helper.js | 5 +++ packages/table/src/store/index.js | 5 ++- packages/table/src/table-layout.js | 53 ++++++++++++++---------------- packages/table/src/table.vue | 35 +++++++++++--------- packages/table/src/util.js | 15 ++++++++- 5 files changed, 64 insertions(+), 49 deletions(-) diff --git a/packages/table/src/store/helper.js b/packages/table/src/store/helper.js index 393660c2c8e..4cf271f7363 100644 --- a/packages/table/src/store/helper.js +++ b/packages/table/src/store/helper.js @@ -1,4 +1,5 @@ import Store from './index'; +let id = 0; export function createStore(table, initialState = {}) { if (!table) { @@ -7,12 +8,16 @@ export function createStore(table, initialState = {}) { const store = new Store(); store.table = table; + store.id = id++; Object.keys(initialState).forEach(key => { store.states[key] = initialState[key]; }); Object.keys(store.modifiers).forEach(key => { store.modifiers[key] = store.modifiers[key].bind(store); }); + // Object.keys(store.modifiers).forEach(key => { + // store.modifiers[key] = store.modifiers[key].bind(store); + // }); return store; } diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js index dca1f089e24..1b54df5cd3e 100644 --- a/packages/table/src/store/index.js +++ b/packages/table/src/store/index.js @@ -147,9 +147,8 @@ Wachter.prototype.commit = function(name, ...args) { // 额外的 DOM 操作都放在 modifiers 中 Wachter.prototype.modifiers = { updateScrollY() { - Vue.nextTick(() => { - this.table.updateScrollY(); - }); + console.log('this.id', this.id); + Vue.nextTick(this.table.updateScrollY); } }; diff --git a/packages/table/src/table-layout.js b/packages/table/src/table-layout.js index a7a14a1c6c2..5b78519da81 100644 --- a/packages/table/src/table-layout.js +++ b/packages/table/src/table-layout.js @@ -1,5 +1,6 @@ -import scrollbarWidth from 'element-ui/src/utils/scrollbar-width'; import Vue from 'vue'; +import scrollbarWidth from 'element-ui/src/utils/scrollbar-width'; +import { parseHeight } from './util'; class TableLayout { constructor(options) { @@ -41,7 +42,7 @@ class TableLayout { updateScrollY() { const height = this.height; - if (typeof height !== 'string' && typeof height !== 'number') return; + if (height === null) return; const bodyWrapper = this.table.bodyWrapper; if (this.table.$el && bodyWrapper) { const body = bodyWrapper.querySelector('.el-table__body'); @@ -52,25 +53,33 @@ class TableLayout { setHeight(value, prop = 'height') { if (Vue.prototype.$isServer) return; const el = this.table.$el; - if (typeof value === 'string' && /^\d+$/.test(value)) { - value = Number(value); - } + value = parseHeight(value); this.height = value; if (!el && (value || value === 0)) return Vue.nextTick(() => this.setHeight(value, prop)); - if (typeof value === 'number') { - el.style[prop] = value + 'px'; - - this.updateElsHeight(); - } else if (typeof value === 'string') { - el.style[prop] = value; + if (value) { + el.style[prop] = `${value}px`; this.updateElsHeight(); } } setMaxHeight(value) { - return this.setHeight(value, 'max-height'); + this.setHeight(value, 'max-height'); + } + + getFlattenColumns() { + const flattenColumns = []; + const columns = this.table.columns; + columns.forEach((column) => { + if (column.isColumnGroup) { + flattenColumns.push.apply(flattenColumns, column.columns); + } else { + flattenColumns.push(column); + } + }); + + return flattenColumns; } updateElsHeight() { @@ -84,11 +93,11 @@ class TableLayout { return Vue.nextTick(() => this.updateElsHeight()); } const tableHeight = this.tableHeight = this.table.$el.clientHeight; - if (this.height !== null && (!isNaN(this.height) || typeof this.height === 'string')) { - const footerHeight = this.footerHeight = footerWrapper ? footerWrapper.offsetHeight : 0; + const footerHeight = this.footerHeight = footerWrapper ? footerWrapper.offsetHeight : 0; + if (this.height !== null) { this.bodyHeight = tableHeight - headerHeight - footerHeight + (footerWrapper ? 1 : 0); } - this.fixedBodyHeight = this.scrollX ? this.bodyHeight - this.gutterWidth : this.bodyHeight; + this.fixedBodyHeight = this.scrollX ? (this.bodyHeight - this.gutterWidth) : this.bodyHeight; const noData = !this.table.data || this.table.data.length === 0; this.viewportHeight = this.scrollX ? tableHeight - (noData ? 0 : this.gutterWidth) : tableHeight; @@ -97,20 +106,6 @@ class TableLayout { this.notifyObservers('scrollable'); } - getFlattenColumns() { - const flattenColumns = []; - const columns = this.table.columns; - columns.forEach((column) => { - if (column.isColumnGroup) { - flattenColumns.push.apply(flattenColumns, column.columns); - } else { - flattenColumns.push(column); - } - }); - - return flattenColumns; - } - updateColumnsWidth() { if (Vue.prototype.$isServer) return; const fit = this.fit; diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index e1440bf46d2..5217cbb355a 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -224,6 +224,7 @@ import TableBody from './table-body'; import TableHeader from './table-header'; import TableFooter from './table-footer'; + import { parseHeight } from './util'; let tableIdSeed = 1; @@ -496,14 +497,18 @@ }, bodyHeight() { + const { headerHeight = 0, bodyHeight, footerHeight = 0} = this.layout; if (this.height) { return { - height: this.layout.bodyHeight ? this.layout.bodyHeight + 'px' : '' + height: bodyHeight ? bodyHeight + 'px' : '' }; } else if (this.maxHeight) { - return { - 'max-height': this.layout.bodyHeight ? this.layout.bodyHeight + 'px' : '' - }; + const maxHeight = parseHeight(this.maxHeight); + if (maxHeight) { + return { + 'max-height': (maxHeight - footerHeight - (this.showHeader ? headerHeight : 0)) + 'px' + }; + } } return {}; }, @@ -514,19 +519,18 @@ height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : '' }; } else if (this.maxHeight) { - let maxHeight = this.layout.scrollX ? this.maxHeight - this.layout.gutterWidth : this.maxHeight; - - if (this.showHeader) { - maxHeight -= this.layout.headerHeight; + let maxHeight = parseHeight(this.maxHeight); + if (maxHeight) { + maxHeight = this.layout.scrollX ? maxHeight - this.layout.gutterWidth : maxHeight; + if (this.showHeader) { + maxHeight -= this.layout.headerHeight; + } + maxHeight -= this.layout.footerHeight; + return { + 'max-height': maxHeight + 'px' + }; } - - maxHeight -= this.layout.footerHeight; - - return { - 'max-height': maxHeight + 'px' - }; } - return {}; }, @@ -648,7 +652,6 @@ childrenColumnName: children }); const layout = new TableLayout({ - // store, store: this.store, table: this, fit: this.fit, diff --git a/packages/table/src/util.js b/packages/table/src/util.js index f4171d10cd2..4d09de54b5b 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -158,7 +158,7 @@ export function parseWidth(width) { } } return width; -}; +} export function parseMinWidth(minWidth) { if (typeof minWidth !== 'undefined') { @@ -170,6 +170,19 @@ export function parseMinWidth(minWidth) { return minWidth; }; +export function parseHeight(height) { + if (typeof height === 'number') { + return height; + } + if (typeof height === 'string') { + if (/^\d+(?:px)?/.test(height)) { + return parseInt(height, 10); + } + console.warn('[Element Warn][ElTable]invalid height and it will be ignored.'); + } + return null; +} + // https://github.com/reduxjs/redux/blob/master/src/compose.js export function compose(...funcs) { if (funcs.length === 0) { From 210b2248750f99262e12fa9d7149078d80c21112 Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Tue, 28 May 2019 20:07:04 +0800 Subject: [PATCH 09/18] add loading icon --- packages/table/src/config.js | 13 ++++++++----- packages/table/src/store/helper.js | 8 -------- packages/table/src/store/index.js | 24 +++++++++--------------- packages/table/src/store/tree.js | 16 ++++++++++++---- packages/table/src/store/watcher.js | 1 - packages/table/src/table-body.js | 8 ++++++-- packages/theme-chalk/src/table.scss | 8 +++++--- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/table/src/config.js b/packages/table/src/config.js index 31f674040b8..95929b142f7 100644 --- a/packages/table/src/config.js +++ b/packages/table/src/config.js @@ -11,7 +11,6 @@ export const cellStarts = { order: '', className: 'el-table-column--selection' }, - // TODO: 考虑移除 expand: { width: 48, minWidth: 48, @@ -64,7 +63,6 @@ export const cellForced = { }, sortable: false }, - // TODO: 考虑移除 expand: { renderHeader: function(h, { column }) { return column.label || ''; @@ -109,9 +107,14 @@ export function treeCellPrefix(h, { row, treeNode, store }) { ele.push(); } if (typeof treeNode.expanded === 'boolean') { - ele.push(
- + const expandClasses = ['el-table__expand-icon', treeNode.expanded ? 'el-table__expand-icon--expanded' : '']; + let iconClasses = ['el-icon-arrow-right']; + if (treeNode.loading) { + iconClasses = ['el-icon-loading']; + } + ele.push(
+
); } else { ele.push(); diff --git a/packages/table/src/store/helper.js b/packages/table/src/store/helper.js index 4cf271f7363..81d73f6042d 100644 --- a/packages/table/src/store/helper.js +++ b/packages/table/src/store/helper.js @@ -1,5 +1,4 @@ import Store from './index'; -let id = 0; export function createStore(table, initialState = {}) { if (!table) { @@ -8,16 +7,9 @@ export function createStore(table, initialState = {}) { const store = new Store(); store.table = table; - store.id = id++; Object.keys(initialState).forEach(key => { store.states[key] = initialState[key]; }); - Object.keys(store.modifiers).forEach(key => { - store.modifiers[key] = store.modifiers[key].bind(store); - }); - // Object.keys(store.modifiers).forEach(key => { - // store.modifiers[key] = store.modifiers[key].bind(store); - // }); return store; } diff --git a/packages/table/src/store/index.js b/packages/table/src/store/index.js index 1b54df5cd3e..a5ccd36dd65 100644 --- a/packages/table/src/store/index.js +++ b/packages/table/src/store/index.js @@ -1,8 +1,8 @@ import Vue from 'vue'; -import Wachter from './watcher'; +import Watcher from './watcher'; import { arrayFind } from 'element-ui/src/utils/util'; -Wachter.prototype.mutations = { +Watcher.prototype.mutations = { setData(states, data) { const dataInstanceChanged = states._data !== data; states._data = data; @@ -24,7 +24,7 @@ Wachter.prototype.mutations = { } this.updateAllSelected(); - this.modifiers.updateScrollY(); + this.updateTableScrollY(); }, insertColumn(states, column, index, parent) { @@ -94,7 +94,7 @@ Wachter.prototype.mutations = { }); } - this.modifiers.updateScrollY(); + this.updateTableScrollY(); }, filterChange(states, options) { @@ -107,7 +107,7 @@ Wachter.prototype.mutations = { this.table.$emit('filter-change', newFilters); } - this.modifiers.updateScrollY(); + this.updateTableScrollY(); }, toggleAllSelection() { @@ -119,12 +119,10 @@ Wachter.prototype.mutations = { this.updateAllSelected(); }, - // throttle setHoverRow(states, row) { states.hoverRow = row; }, - // throttle setCurrentRow(states, row) { const oldCurrentRow = states.currentRow; states.currentRow = row; @@ -135,7 +133,7 @@ Wachter.prototype.mutations = { } }; -Wachter.prototype.commit = function(name, ...args) { +Watcher.prototype.commit = function(name, ...args) { const mutations = this.mutations; if (mutations[name]) { mutations[name].apply(this, [this.states].concat(args)); @@ -144,12 +142,8 @@ Wachter.prototype.commit = function(name, ...args) { } }; -// 额外的 DOM 操作都放在 modifiers 中 -Wachter.prototype.modifiers = { - updateScrollY() { - console.log('this.id', this.id); - Vue.nextTick(this.table.updateScrollY); - } +Watcher.prototype.updateTableScrollY = function() { + Vue.nextTick(this.table.updateScrollY); }; -export default Wachter; +export default Watcher; diff --git a/packages/table/src/store/tree.js b/packages/table/src/store/tree.js index c83d544b01f..5363653b4de 100644 --- a/packages/table/src/store/tree.js +++ b/packages/table/src/store/tree.js @@ -20,13 +20,13 @@ export default { computed: { // 嵌入型的数据,watch 无法是检测到变化 https://github.com/ElemeFE/element/issues/14998 // TODO: 使用 computed 解决该问题,是否会造成性能问题? - // @return { id: { level, children, loaded? }} + // @return { id: { level, children } } normalizedData() { if (!this.states.rowKey) return {}; const data = this.states.data || []; return this.normalize(data); }, - // @return { id: { level, children, loaded? } } + // @return { id: { children } } // 针对懒加载的情形,不处理嵌套数据 normalizedLazyNode() { const { rowKey, lazyTreeNodeMap, lazyColumnIndentifier } = this.states; @@ -98,7 +98,9 @@ export default { const newValue = { ...nested[key] }; newValue.expanded = getExpanded(oldValue, key); if (newValue.lazy) { - newValue.loaded = !!(oldValue && oldValue.loaded); + const { loaded = false, loading = false } = oldValue || {}; + newValue.loaded = !!loaded; + newValue.loading = !!loading; rootLazyRowKeys.push(key); } newTreeData[key] = newValue; @@ -116,8 +118,10 @@ export default { } newTreeData[key].children = lazyNodeChildren; } else { + const { loaded = false, loading = false } = oldValue || {}; newTreeData[key] = { - loaded: !!(oldValue && oldValue.loaded), + loaded: !!loaded, + loading: !!loading, expanded: getExpanded(oldValue, key), children: lazyNodeChildren, level: '' @@ -126,6 +130,7 @@ export default { }); } this.states.treeData = newTreeData; + this.updateTableScrollY(); }, updateTreeExpandKeys(value) { @@ -149,6 +154,7 @@ export default { if (oldExpanded !== expanded) { this.table.$emit('expand-change', row); } + this.updateTableScrollY(); } }, @@ -168,10 +174,12 @@ export default { const { load } = this.table; const { lazyTreeNodeMap, treeData } = this.states; if (load && !treeData[key].loaded) { + treeData[key].loading = true; load(row, treeNode, (data) => { if (!Array.isArray(data)) { throw new Error('[ElTable] data must be an array'); } + treeData[key].loading = false; treeData[key].loaded = true; treeData[key].expanded = true; if (data.length) { diff --git a/packages/table/src/store/watcher.js b/packages/table/src/store/watcher.js index 16fded778cf..66b9b67017c 100644 --- a/packages/table/src/store/watcher.js +++ b/packages/table/src/store/watcher.js @@ -6,7 +6,6 @@ import expand from './expand'; import current from './current'; import tree from './tree'; -// import const sortData = (data, states) => { const sortingColumn = states.sortingColumn; if (!sortingColumn || typeof sortingColumn.sortable === 'string') { diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index 7d3c5641970..1d5d9a32c9f 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -344,12 +344,13 @@ export default { data.treeNode = { indent: treeRowData.level * treeIndent, level: treeRowData.level - // loaded: treeNode.loaded }; if (typeof treeRowData.expanded === 'boolean') { data.treeNode.expanded = treeRowData.expanded; + if (typeof treeRowData.loading === 'boolean') { + data.treeNode.loading = treeRowData.loading; + } } - // console.log('data treeNode ', data.treeNode); } return (
Date: Tue, 28 May 2019 20:07:38 +0800 Subject: [PATCH 10/18] update test case --- test/unit/specs/table.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/specs/table.spec.js b/test/unit/specs/table.spec.js index 42256fa90ce..354acab7ca1 100644 --- a/test/unit/specs/table.spec.js +++ b/test/unit/specs/table.spec.js @@ -91,9 +91,9 @@ describe('Table', () => { }); it('height as string', done => { - const vm = createTable('height="100pt"'); + const vm = createTable('height="100px"'); setTimeout(_ => { - expect(vm.$el.style.height).to.equal('100pt'); + expect(vm.$el.style.height).to.equal('100px'); destroyVM(vm); done(); }, DELAY); From 402afc6498dabe30ccd06ae3ae652c824a6c3e6e Mon Sep 17 00:00:00 2001 From: zhiyang-deng Date: Wed, 29 May 2019 16:46:32 +0800 Subject: [PATCH 11/18] update docs and add test cases --- examples/docs/en-US/table.md | 42 ++--- examples/docs/es/table.md | 38 +++-- examples/docs/fr-FR/table.md | 46 ++--- examples/docs/zh-CN/table.md | 45 ++--- packages/table/src/store/tree.js | 15 +- packages/table/src/store/watcher.js | 2 +- packages/table/src/table.vue | 2 +- test/unit/specs/table.spec.js | 252 ++++++++++++++++++++-------- 8 files changed, 283 insertions(+), 159 deletions(-) diff --git a/examples/docs/en-US/table.md b/examples/docs/en-US/table.md index 52573628b53..8d1c42e5a62 100644 --- a/examples/docs/en-US/table.md +++ b/examples/docs/en-US/table.md @@ -1332,7 +1332,7 @@ When the row content is too long and you do not want to display the horizontal s ### Tree data and lazy mode -:::demo You can display tree structure data. When using it, the prop `row-key` is required. Also, child row data can be loaded asynchronously. Set `lazy` property of Table to true and the function `load`. Specify `hasChildren` attribute in row to determine which row contains children. +:::demo You can display tree structure data. When row contains the `children` field, it is treated as nested data. For rendering nested data, the prop `row-key` is required。Also, child row data can be loaded asynchronously. Set `lazy` property of Table to true and the function `load`. Specify `hasChildren` attribute in row to determine which row contains children. Both `children` and `hasChildren` can be configured via `tree-props`. ```html