This commit is contained in:
Ivan
2021-06-07 11:56:04 +08:00
commit c3c9fee2fb
1071 changed files with 195655 additions and 0 deletions

8
packages/table/index.js Normal file
View File

@@ -0,0 +1,8 @@
import ElTable from './src/table';
/* istanbul ignore next */
ElTable.install = function(Vue) {
Vue.component(ElTable.name, ElTable);
};
export default ElTable;

View File

@@ -0,0 +1,123 @@
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'
},
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 <el-checkbox
disabled={ store.states.data && store.states.data.length === 0 }
indeterminate={ store.states.selection.length > 0 && !this.isAllSelected }
nativeOn-click={ this.toggleAllSelection }
value={ this.isAllSelected } />;
},
renderCell: function(h, { row, column, store, $index }) {
return <el-checkbox
nativeOn-click={ (event) => 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 <div>{ i }</div>;
},
sortable: false
},
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 (<div class={ classes }
on-click={callback}>
<i class='el-icon el-icon-arrow-right'></i>
</div>);
},
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;
}
export function treeCellPrefix(h, { row, treeNode, store }) {
if (!treeNode) return null;
const ele = [];
const callback = function(e) {
e.stopPropagation();
store.loadOrToggle(row);
};
if (treeNode.indent) {
ele.push(<span class="el-table__indent" style={{'padding-left': treeNode.indent + 'px'}}></span>);
}
if (typeof treeNode.expanded === 'boolean' && !treeNode.noLazyChildren) {
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(<div class={ expandClasses }
on-click={ callback }>
<i class={ iconClasses }></i>
</div>);
} else {
ele.push(<span class="el-table__placeholder"></span>);
}
return ele;
}

View File

@@ -0,0 +1,28 @@
import Vue from 'vue';
var dropdowns = [];
!Vue.prototype.$isServer && document.addEventListener('click', function(event) {
dropdowns.forEach(function(dropdown) {
var target = event.target;
if (!dropdown || !dropdown.$el) return;
if (target === dropdown.$el || dropdown.$el.contains(target)) {
return;
}
dropdown.handleOutsideClick && dropdown.handleOutsideClick(event);
});
});
export default {
open(instance) {
if (instance) {
dropdowns.push(instance);
}
},
close(instance) {
var index = dropdowns.indexOf(instance);
if (index !== -1) {
dropdowns.splice(instance, 1);
}
}
};

View File

@@ -0,0 +1,194 @@
<template>
<transition name="el-zoom-in-top">
<div
class="el-table-filter"
v-if="multiple"
v-clickoutside="handleOutsideClick"
v-show="showPopper">
<div class="el-table-filter__content">
<el-scrollbar wrap-class="el-table-filter__wrap">
<el-checkbox-group class="el-table-filter__checkbox-group" v-model="filteredValue">
<el-checkbox
v-for="filter in filters"
:key="filter.value"
:label="filter.value">{{ filter.text }}</el-checkbox>
</el-checkbox-group>
</el-scrollbar>
</div>
<div class="el-table-filter__bottom">
<button @click="handleConfirm"
:class="{ 'is-disabled': filteredValue.length === 0 }"
:disabled="filteredValue.length === 0">{{ t('el.table.confirmFilter') }}</button>
<button @click="handleReset">{{ t('el.table.resetFilter') }}</button>
</div>
</div>
<div
class="el-table-filter"
v-else
v-clickoutside="handleOutsideClick"
v-show="showPopper">
<ul class="el-table-filter__list">
<li class="el-table-filter__list-item"
:class="{ 'is-active': filterValue === undefined || filterValue === null }"
@click="handleSelect(null)">{{ t('el.table.clearFilter') }}</li>
<li class="el-table-filter__list-item"
v-for="filter in filters"
:label="filter.value"
:key="filter.value"
:class="{ 'is-active': isActive(filter) }"
@click="handleSelect(filter.value)" >{{ filter.text }}</li>
</ul>
</div>
</transition>
</template>
<script type="text/babel">
import Popper from 'element-ui/src/utils/vue-popper';
import { PopupManager } from 'element-ui/src/utils/popup';
import Locale from 'element-ui/src/mixins/locale';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Dropdown from './dropdown';
import ElCheckbox from 'element-ui/packages/checkbox';
import ElCheckboxGroup from 'element-ui/packages/checkbox-group';
import ElScrollbar from 'element-ui/packages/scrollbar';
export default {
name: 'ElTableFilterPanel',
mixins: [Popper, Locale],
directives: {
Clickoutside
},
components: {
ElCheckbox,
ElCheckboxGroup,
ElScrollbar
},
props: {
placement: {
type: String,
default: 'bottom-end'
}
},
methods: {
isActive(filter) {
return filter.value === this.filterValue;
},
handleOutsideClick() {
setTimeout(() => {
this.showPopper = false;
}, 16);
},
handleConfirm() {
this.confirmFilter(this.filteredValue);
this.handleOutsideClick();
},
handleReset() {
this.filteredValue = [];
this.confirmFilter(this.filteredValue);
this.handleOutsideClick();
},
handleSelect(filterValue) {
this.filterValue = filterValue;
if ((typeof filterValue !== 'undefined') && (filterValue !== null)) {
this.confirmFilter(this.filteredValue);
} else {
this.confirmFilter([]);
}
this.handleOutsideClick();
},
confirmFilter(filteredValue) {
this.table.store.commit('filterChange', {
column: this.column,
values: filteredValue
});
this.table.store.updateAllSelected();
}
},
data() {
return {
table: null,
cell: null,
column: null
};
},
computed: {
filters() {
return this.column && this.column.filters;
},
filterValue: {
get() {
return (this.column.filteredValue || [])[0];
},
set(value) {
if (this.filteredValue) {
if ((typeof value !== 'undefined') && (value !== null)) {
this.filteredValue.splice(0, 1, value);
} else {
this.filteredValue.splice(0, 1);
}
}
}
},
filteredValue: {
get() {
if (this.column) {
return this.column.filteredValue || [];
}
return [];
},
set(value) {
if (this.column) {
this.column.filteredValue = value;
}
}
},
multiple() {
if (this.column) {
return this.column.filterMultiple;
}
return true;
}
},
mounted() {
this.popperElm = this.$el;
this.referenceElm = this.cell;
this.table.bodyWrapper.addEventListener('scroll', () => {
this.updatePopper();
});
this.$watch('showPopper', (value) => {
if (this.column) this.column.filterOpened = value;
if (value) {
Dropdown.open(this);
} else {
Dropdown.close(this);
}
});
},
watch: {
showPopper(val) {
if (val === true && parseInt(this.popperJS._popper.style.zIndex, 10) < PopupManager.zIndex) {
this.popperJS._popper.style.zIndex = PopupManager.nextZIndex();
}
}
}
};
</script>

View File

@@ -0,0 +1,68 @@
export default {
created() {
this.tableLayout.addObserver(this);
},
destroyed() {
this.tableLayout.removeObserver(this);
},
computed: {
tableLayout() {
let layout = this.layout;
if (!layout && this.table) {
layout = this.table.layout;
}
if (!layout) {
throw new Error('Can not find table layout.');
}
return layout;
}
},
mounted() {
this.onColumnsChange(this.tableLayout);
this.onScrollableChange(this.tableLayout);
},
updated() {
if (this.__updated__) return;
this.onColumnsChange(this.tableLayout);
this.onScrollableChange(this.tableLayout);
this.__updated__ = true;
},
methods: {
onColumnsChange(layout) {
const cols = this.$el.querySelectorAll('colgroup > col');
if (!cols.length) return;
const flattenColumns = layout.getFlattenColumns();
const columnsMap = {};
flattenColumns.forEach((column) => {
columnsMap[column.id] = column;
});
for (let i = 0, j = cols.length; i < j; i++) {
const col = cols[i];
const name = col.getAttribute('name');
const column = columnsMap[name];
if (column) {
col.setAttribute('width', column.realWidth || column.width);
}
}
},
onScrollableChange(layout) {
const cols = this.$el.querySelectorAll('colgroup > col[name=gutter]');
for (let i = 0, j = cols.length; i < j; i++) {
const col = cols[i];
col.setAttribute('width', layout.scrollY ? layout.gutterWidth : '0');
}
const ths = this.$el.querySelectorAll('th.gutter');
for (let i = 0, j = ths.length; i < j; i++) {
const th = ths[i];
th.style.width = layout.scrollY ? layout.gutterWidth + 'px' : '0';
th.style.display = layout.scrollY ? '' : 'none';
}
}
}
};

View File

@@ -0,0 +1,76 @@
import { arrayFind } from 'element-ui/src/utils/util';
import { getRowIdentity } from '../util';
export default {
data() {
return {
states: {
// 不可响应的,设置 currentRowKey 时data 不一定存在,也许无法算出正确的 currentRow
// 把该值缓存一下,当用户点击修改 currentRow 时,把该值重置为 null
_currentRowKey: null,
currentRow: null
}
};
},
methods: {
setCurrentRowKey(key) {
this.assertRowKey();
this.states._currentRowKey = key;
this.setCurrentRowByKey(key);
},
restoreCurrentRowKey() {
this.states._currentRowKey = null;
},
setCurrentRowByKey(key) {
const { states } = this;
const { data = [], rowKey } = states;
let currentRow = null;
if (rowKey) {
currentRow = arrayFind(data, item => getRowIdentity(item, rowKey) === key);
}
states.currentRow = currentRow;
},
updateCurrentRow(currentRow) {
const { states, table } = this;
const oldCurrentRow = states.currentRow;
if (currentRow && currentRow !== oldCurrentRow) {
states.currentRow = currentRow;
table.$emit('current-change', currentRow, oldCurrentRow);
return;
}
if (!currentRow && oldCurrentRow) {
states.currentRow = null;
table.$emit('current-change', null, oldCurrentRow);
}
},
updateCurrentRowData() {
const { states, table } = this;
const { rowKey, _currentRowKey } = states;
// data 为 null 时,解构时的默认值会被忽略
const data = states.data || [];
const oldCurrentRow = states.currentRow;
// 当 currentRow 不在 data 中时尝试更新数据
if (data.indexOf(oldCurrentRow) === -1 && oldCurrentRow) {
if (rowKey) {
const currentRowKey = getRowIdentity(oldCurrentRow, rowKey);
this.setCurrentRowByKey(currentRowKey);
} else {
states.currentRow = null;
}
if (states.currentRow === null) {
table.$emit('current-change', null, oldCurrentRow);
}
} else if (_currentRowKey) {
// 把初始时下设置的 rowKey 转化成 rowData
this.setCurrentRowByKey(_currentRowKey);
this.restoreCurrentRowKey();
}
}
}
};

View File

@@ -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.slice());
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;
}
}
};

View File

@@ -0,0 +1,41 @@
import Store from './index';
import debounce from 'throttle-debounce/debounce';
export function createStore(table, initialState = {}) {
if (!table) {
throw new Error('Table is required.');
}
const store = new Store();
store.table = table;
// fix https://github.com/ElemeFE/element/issues/14075
// related pr https://github.com/ElemeFE/element/pull/14146
store.toggleAllSelection = debounce(10, store._toggleAllSelection);
Object.keys(initialState).forEach(key => {
store.states[key] = initialState[key];
});
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('invalid value type');
}
if (fn) {
res[key] = fn;
}
});
return res;
};

View File

@@ -0,0 +1,147 @@
import Vue from 'vue';
import Watcher from './watcher';
import { arrayFind } from 'element-ui/src/utils/util';
Watcher.prototype.mutations = {
setData(states, data) {
const dataInstanceChanged = states._data !== data;
states._data = data;
this.execQuery();
// 数据变化,更新部分数据。
// 没有使用 computed而是手动更新部分数据 https://github.com/vuejs/vue/issues/6660#issuecomment-331417140
this.updateCurrentRowData();
this.updateExpandRows();
if (states.reserveSelection) {
this.assertRowKey();
this.updateSelectionByRowKey();
} else {
if (dataInstanceChanged) {
this.clearSelection();
} else {
this.cleanSelection();
}
}
this.updateAllSelected();
this.updateTableScrollY();
},
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, init } = options;
if (prop) {
const column = arrayFind(states.columns, column => column.property === prop);
if (column) {
column.order = order;
this.updateSort(column, prop, order);
this.commit('changeSortCondition', { init });
}
}
},
changeSortCondition(states, options) {
// 修复 pr https://github.com/ElemeFE/element/pull/15012 导致的 bug
const { sortingColumn: column, sortProp: prop, sortOrder: order } = states;
if (order === null) {
states.sortingColumn = null;
states.sortProp = null;
}
const ingore = { filter: true };
this.execQuery(ingore);
if (!options || !(options.silent || options.init)) {
this.table.$emit('sort-change', {
column,
prop,
order
});
}
this.updateTableScrollY();
},
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.updateTableScrollY();
},
toggleAllSelection() {
this.toggleAllSelection();
},
rowSelectedChanged(states, row) {
this.toggleRowSelection(row);
this.updateAllSelected();
},
setHoverRow(states, row) {
states.hoverRow = row;
},
setCurrentRow(states, row) {
this.updateCurrentRow(row);
}
};
Watcher.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}`);
}
};
Watcher.prototype.updateTableScrollY = function() {
Vue.nextTick(this.table.updateScrollY);
};
export default Watcher;

View File

@@ -0,0 +1,208 @@
import { walkTreeNode, getRowIdentity } from '../util';
export default {
data() {
return {
states: {
// defaultExpandAll 存在于 expand.js 中,这里不重复添加
// 在展开行中expandRowKeys 会被转化成 expandRowsexpandRowKeys 这个属性只是记录了 TreeTable 行的展开
// TODO: 拆分为独立的 TreeTable统一用法
expandRowKeys: [],
treeData: {},
indent: 16,
lazy: false,
lazyTreeNodeMap: {},
lazyColumnIdentifier: 'hasChildren',
childrenColumnName: 'children'
}
};
},
computed: {
// 嵌入型的数据watch 无法是检测到变化 https://github.com/ElemeFE/element/issues/14998
// TODO: 使用 computed 解决该问题,是否会造成性能问题?
// @return { id: { level, children } }
normalizedData() {
if (!this.states.rowKey) return {};
const data = this.states.data || [];
return this.normalize(data);
},
// @return { id: { children } }
// 针对懒加载的情形,不处理嵌套数据
normalizedLazyNode() {
const { rowKey, lazyTreeNodeMap, lazyColumnIdentifier } = this.states;
const keys = Object.keys(lazyTreeNodeMap);
const res = {};
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[lazyColumnIdentifier] && !res[currentRowKey]) {
res[currentRowKey] = { children: [] };
}
});
res[key] = item;
}
});
return res;
}
},
watch: {
normalizedData: 'updateTreeData',
normalizedLazyNode: 'updateTreeData'
},
methods: {
normalize(data) {
const {
childrenColumnName,
lazyColumnIdentifier,
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,
lazyColumnIdentifier
);
return res;
},
updateTreeData() {
const nested = this.normalizedData;
const normalizedLazyNode = this.normalizedLazyNode;
const keys = Object.keys(nested);
const newTreeData = {};
if (keys.length) {
const {
treeData: oldTreeData,
defaultExpandAll,
expandRowKeys,
lazy
} = this.states;
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 oldValue = oldTreeData[key];
const newValue = { ...nested[key] };
newValue.expanded = getExpanded(oldValue, key);
if (newValue.lazy) {
const { loaded = false, loading = false } = oldValue || {};
newValue.loaded = !!loaded;
newValue.loading = !!loading;
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 {
const { loaded = false, loading = false } = oldValue || {};
newTreeData[key] = {
lazy: true,
loaded: !!loaded,
loading: !!loading,
expanded: getExpanded(oldValue, key),
children: lazyNodeChildren,
level: ''
};
}
});
}
}
this.states.treeData = newTreeData;
this.updateTableScrollY();
},
updateTreeExpandKeys(value) {
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];
if (id && data && ('expanded' in data)) {
const oldExpanded = data.expanded;
expanded = typeof expanded === 'undefined' ? !data.expanded : expanded;
treeData[id].expanded = expanded;
if (oldExpanded !== expanded) {
this.table.$emit('expand-change', row, expanded);
}
this.updateTableScrollY();
}
},
loadOrToggle(row) {
this.assertRowKey();
const { lazy, treeData, rowKey } = this.states;
const id = getRowIdentity(row, rowKey);
const data = treeData[id];
if (lazy && data && 'loaded' in data && !data.loaded) {
this.loadData(row, id, data);
} else {
this.toggleTreeExpansion(row);
}
},
loadData(row, key, treeNode) {
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) {
this.$set(lazyTreeNodeMap, key, data);
}
this.table.$emit('expand-change', row, true);
});
}
}
}
};

View File

@@ -0,0 +1,381 @@
import Vue from 'vue';
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';
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
}
};
},
mixins: [expand, current, tree],
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 (oldSelection.length) {
states.selection = [];
this.table.$emit('selection-change', []);
}
},
cleanSelection() {
const states = this.states;
const { data, rowKey, selection } = states;
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 => data.indexOf(item) === -1);
}
if (deleted.length) {
const newSelection = selection.filter(item => deleted.indexOf(item) === -1);
states.selection = newSelection;
this.table.$emit('selection-change', newSelection.slice());
}
},
toggleRowSelection(row, selected, emitChange = true) {
const changed = toggleRowStatus(this.states.selection, row, selected);
if (changed) {
const newSelection = (this.states.selection || []).slice();
// 调用 API 修改选中值,不触发 select 事件
if (emitChange) {
this.table.$emit('select', newSelection, row);
}
this.table.$emit('selection-change', newSelection);
}
},
_toggleAllSelection() {
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);
data.forEach(row => {
const rowId = getRowIdentity(row, rowKey);
const rowInfo = selectedMap[rowId];
if (rowInfo) {
selection[rowInfo.index] = row;
}
});
},
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) {
if (this.states.sortingColumn && this.states.sortingColumn !== column) {
this.states.sortingColumn.order = null;
}
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;
},
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
});
},
// 适配层expand-row-keys 在 Expand 与 TreeTable 中都有使用
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.toggleTreeExpansion(row, expanded);
}
}
}
});

View File

@@ -0,0 +1,483 @@
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';
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',
mixins: [LayoutObserver],
components: {
ElCheckbox,
ElTooltip
},
props: {
store: {
required: true
},
stripe: Boolean,
context: {},
rowClassName: [String, Function],
rowStyle: [Object, Function],
fixed: String,
highlight: Boolean
},
render(h) {
const data = this.data || [];
return (
<table
class="el-table__body"
cellspacing="0"
cellpadding="0"
border="0">
<colgroup>
{
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
</colgroup>
<tbody>
{
data.reduce((acc, row) => {
return acc.concat(this.wrappedRowRender(row, acc.length));
}, [])
}
<el-tooltip effect={ this.table.tooltipEffect } placement="top" ref="tooltip" content={ this.tooltipContent }></el-tooltip>
</tbody>
</table>
);
},
computed: {
table() {
return this.$parent;
},
...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() {
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) {
if (!this.store.states.isComplex || this.$isServer) return;
let raf = window.requestAnimationFrame;
if (!raf) {
raf = (fn) => setTimeout(fn, 16);
}
raf(() => {
const rows = this.$el.querySelectorAll('.el-table__row');
const oldRow = rows[oldVal];
const newRow = rows[newVal];
if (oldRow) {
removeClass(oldRow, 'hover-row');
}
if (newRow) {
addClass(newRow, 'hover-row');
}
});
}
},
data() {
return {
tooltipContent: ''
};
},
created() {
this.activateTooltip = debounce(50, tooltip => tooltip.handleShowPopper());
},
methods: {
getKeyOfRow(row, index) {
const rowKey = this.table.rowKey;
if (rowKey) {
return getRowIdentity(row, rowKey);
}
return index;
},
isColumnHidden(index) {
if (this.fixed === true || this.fixed === 'left') {
return index >= this.leftFixedLeafCount;
} else if (this.fixed === 'right') {
return index < this.columnsCount - this.rightFixedLeafCount;
} else {
return (index < this.leftFixedLeafCount) || (index >= this.columnsCount - this.rightFixedLeafCount);
}
},
getSpan(row, column, rowIndex, columnIndex) {
let rowspan = 1;
let colspan = 1;
const fn = this.table.spanMethod;
if (typeof fn === 'function') {
const result = fn({
row,
column,
rowIndex,
columnIndex
});
if (Array.isArray(result)) {
rowspan = result[0];
colspan = result[1];
} else if (typeof result === 'object') {
rowspan = result.rowspan;
colspan = result.colspan;
}
}
return { rowspan, colspan };
},
getRowStyle(row, rowIndex) {
const rowStyle = this.table.rowStyle;
if (typeof rowStyle === 'function') {
return rowStyle.call(null, {
row,
rowIndex
});
}
return rowStyle || null;
},
getRowClass(row, rowIndex) {
const classes = ['el-table__row'];
if (this.table.highlightCurrentRow && row === this.store.states.currentRow) {
classes.push('current-row');
}
if (this.stripe && rowIndex % 2 === 1) {
classes.push('el-table__row--striped');
}
const rowClassName = this.table.rowClassName;
if (typeof rowClassName === 'string') {
classes.push(rowClassName);
} else if (typeof rowClassName === 'function') {
classes.push(rowClassName.call(null, {
row,
rowIndex
}));
}
if (this.store.states.expandRows.indexOf(row) > -1) {
classes.push('expanded');
}
return classes;
},
getCellStyle(rowIndex, columnIndex, row, column) {
const cellStyle = this.table.cellStyle;
if (typeof cellStyle === 'function') {
return cellStyle.call(null, {
rowIndex,
columnIndex,
row,
column
});
}
return cellStyle;
},
getCellClass(rowIndex, columnIndex, row, column) {
const classes = [column.id, column.align, column.className];
if (this.isColumnHidden(columnIndex)) {
classes.push('is-hidden');
}
const cellClassName = this.table.cellClassName;
if (typeof cellClassName === 'string') {
classes.push(cellClassName);
} else if (typeof cellClassName === 'function') {
classes.push(cellClassName.call(null, {
rowIndex,
columnIndex,
row,
column
}));
}
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);
if (cell) {
const column = getColumnByCell(table, cell);
const hoverState = table.hoverState = {cell, column, row};
table.$emit('cell-mouse-enter', hoverState.row, hoverState.column, hoverState.cell, event);
}
// 判断是否text-overflow, 如果是就显示tooltip
const cellChild = event.target.querySelector('.cell');
if (!(hasClass(cellChild, 'el-tooltip') && cellChild.childNodes.length)) {
return;
}
// use range width instead of scrollWidth to determine whether the text is overflowing
// to address a potential FireFox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1074543#c3
const range = document.createRange();
range.setStart(cellChild, 0);
range.setEnd(cellChild, cellChild.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
(parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
const tooltip = this.$refs.tooltip;
// TODO 会引起整个 Table 的重新渲染,需要优化
this.tooltipContent = cell.innerText || cell.textContent;
tooltip.referenceElm = cell;
tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none');
tooltip.doDestroy();
tooltip.setExpectedState(true);
this.activateTooltip(tooltip);
}
},
handleCellMouseLeave(event) {
const tooltip = this.$refs.tooltip;
if (tooltip) {
tooltip.setExpectedState(false);
tooltip.handleClosePopper();
}
const cell = getCell(event);
if (!cell) return;
const oldHoverState = this.table.hoverState || {};
this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
},
handleMouseEnter: debounce(30, function(index) {
this.store.commit('setHoverRow', index);
}),
handleMouseLeave: debounce(30, function() {
this.store.commit('setHoverRow', null);
}),
handleContextMenu(event, row) {
this.handleEvent(event, row, 'contextmenu');
},
handleDoubleClick(event, row) {
this.handleEvent(event, row, 'dblclick');
},
handleClick(event, row) {
this.store.commit('setCurrentRow', row);
this.handleEvent(event, row, 'click');
},
handleEvent(event, row, name) {
const table = this.table;
const cell = getCell(event);
let column;
if (cell) {
column = getColumnByCell(table, cell);
if (column) {
table.$emit(`cell-${name}`, row, column, cell, event);
}
}
table.$emit(`row-${name}`, row, column, event);
},
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;
}
// 指令 v-show 会覆盖 row-style 中 display
// 使用 :style 代替 v-show https://github.com/ElemeFE/element/issues/16995
let displayStyle = display ? null : {
display: 'none'
};
return (<tr
style={ [displayStyle, this.getRowStyle(row, $index)] }
class={ rowClasses }
key={ this.getKeyOfRow(row, $index) }
on-dblclick={ ($event) => 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 }>
{
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
};
if (typeof treeRowData.expanded === 'boolean') {
data.treeNode.expanded = treeRowData.expanded;
// 表明是懒加载
if ('loading' in treeRowData) {
data.treeNode.loading = treeRowData.loading;
}
if ('noLazyChildren' in treeRowData) {
data.treeNode.noLazyChildren = treeRowData.noLazyChildren;
}
}
}
return (
<td
style={ this.getCellStyle($index, cellIndex, row, column) }
class={ this.getCellClass($index, cellIndex, row, column) }
rowspan={ rowspan }
colspan={ colspan }
on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
on-mouseleave={ this.handleCellMouseLeave }>
{
column.renderCell.call(
this._renderProxy,
this.$createElement,
data,
columnsHidden[cellIndex]
)
}
</td>
);
})
}
</tr>);
},
wrappedRowRender(row, $index) {
const store = this.store;
const { isRowExpanded, assertRowKey } = store;
const { treeData, lazyTreeNodeMap, childrenColumnName, rowKey } = store.states;
if (this.hasExpandColumn && isRowExpanded(row)) {
const renderExpanded = this.table.renderExpanded;
const tr = this.rowRender(row, $index);
if (!renderExpanded) {
console.error('[Element Error]renderExpanded is required.');
return tr;
}
// 使用二维数组,避免修改 $index
return [[
tr,
<tr key={'expanded-row__' + tr.key}>
<td colspan={ this.columnsCount } class="el-table__expanded-cell">
{ renderExpanded(this.$createElement, { row, $index, store: this.store }) }
</td>
</tr>]];
} 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
};
if (typeof cur.lazy === 'boolean') {
if (typeof cur.loaded === 'boolean' && cur.loaded) {
treeRowData.noLazyChildren = !(cur.children && cur.children.length);
}
treeRowData.loading = cur.loading;
}
}
const tmp = [this.rowRender(row, $index, treeRowData)];
// 渲染嵌套数据
if (cur) {
// currentRow 记录的是 index所以还需主动增加 TreeTable 的 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
};
const childKey = getRowIdentity(node, rowKey);
if (childKey === undefined || childKey === null) {
throw new Error('for nested data item, row-key is required.');
}
cur = { ...treeData[childKey] };
// 对于当前节点,分成有无子节点两种情况。
// 如果包含子节点的,设置 expanded 属性。
// 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定。
if (cur) {
innerTreeRowData.expanded = cur.expanded;
// 懒加载的某些节点level 未知
cur.level = cur.level || innerTreeRowData.level;
cur.display = !!(cur.expanded && innerTreeRowData.display);
if (typeof cur.lazy === 'boolean') {
if (typeof cur.loaded === 'boolean' && cur.loaded) {
innerTreeRowData.noLazyChildren = !(cur.children && cur.children.length);
}
innerTreeRowData.loading = cur.loading;
}
}
i++;
tmp.push(this.rowRender(node, $index + i, innerTreeRowData));
if (cur) {
const nodes = lazyTreeNodeMap[childKey] || node[childrenColumnName];
traverse(nodes, cur);
}
});
};
// 对于 root 节点display 一定为 true
cur.display = true;
const nodes = lazyTreeNodeMap[key] || row[childrenColumnName];
traverse(nodes, cur);
}
return tmp;
} else {
return this.rowRender(row, $index);
}
}
}
};

View File

@@ -0,0 +1,319 @@
import { cellStarts, cellForced, defaultRenderCell, treeCellPrefix } from './config';
import { mergeOptions, parseWidth, parseMinWidth, compose } from './util';
import ElCheckbox from 'element-ui/packages/checkbox';
let columnIdSeed = 1;
export default {
name: 'ElTableColumn',
props: {
type: {
type: String,
default: 'default'
},
label: String,
className: String,
labelClassName: String,
property: String,
prop: String,
width: {},
minWidth: {},
renderHeader: Function,
sortable: {
type: [Boolean, String],
default: false
},
sortMethod: Function,
sortBy: [String, Function, Array],
resizable: {
type: Boolean,
default: true
},
columnKey: String,
align: String,
headerAlign: String,
showTooltipWhenOverflow: Boolean,
showOverflowTooltip: Boolean,
fixed: [Boolean, String],
formatter: Function,
selectable: Function,
reserveSelection: Boolean,
filterMethod: Function,
filteredValue: Array,
filters: Array,
filterPlacement: String,
filterMultiple: {
type: Boolean,
default: true
},
index: [Number, Function],
sortOrders: {
type: Array,
default() {
return ['ascending', 'descending', null];
},
validator(val) {
return val.every(order => ['ascending', 'descending', null].indexOf(order) > -1);
}
}
},
data() {
return {
isSubColumn: false,
columns: []
};
},
computed: {
owner() {
let parent = this.$parent;
while (parent && !parent.tableId) {
parent = parent.$parent;
}
return parent;
},
columnOrTableParent() {
let parent = this.$parent;
while (parent && !parent.tableId && !parent.columnId) {
parent = parent.$parent;
}
return parent;
},
realWidth() {
return parseWidth(this.width);
},
realMinWidth() {
return parseMinWidth(this.minWidth);
},
realAlign() {
return this.align ? 'is-' + this.align : null;
},
realHeaderAlign() {
return this.headerAlign ? 'is-' + this.headerAlign : this.realAlign;
}
},
methods: {
getPropsData(...props) {
return props.reduce((prev, cur) => {
if (Array.isArray(cur)) {
cur.forEach((key) => {
prev[key] = this[key];
});
}
return prev;
}, {});
},
getColumnElIndex(children, child) {
return [].indexOf.call(children, child);
},
setColumnWidth(column) {
if (this.realWidth) {
column.width = this.realWidth;
}
if (this.realMinWidth) {
column.minWidth = this.realMinWidth;
}
if (!column.minWidth) {
column.minWidth = 80;
}
column.realWidth = column.width === undefined ? column.minWidth : column.width;
return column;
},
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;
},
setColumnRenders(column) {
// 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 (column.type !== 'selection') {
column.renderHeader = (h, scope) => {
const renderHeader = this.$scopedSlots.header;
return renderHeader ? renderHeader(scope) : column.label;
};
}
let originRenderCell = column.renderCell;
// TODO: 这里的实现调整
if (column.type === 'expand') {
// 对于展开行renderCell 不允许配置的。在上一步中已经设置过,这里需要简单封装一下。
column.renderCell = (h, data) => (<div class="cell">
{ originRenderCell(h, data) }
</div>);
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 prefix = treeCellPrefix(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 (<div { ...props }>
{ prefix }
{ children }
</div>);
};
}
return column;
},
registerNormalWatchers() {
const props = ['label', 'property', 'filters', 'filterMultiple', 'sortable', 'index', 'formatter', 'className', 'labelClassName', 'showOverflowTooltip'];
// 一些属性具有别名
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;
});
});
},
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);
});
});
}
},
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,
// index 列
index: this.index
};
const basicProps = ['columnKey', 'label', 'className', 'labelClassName', 'type', 'renderHeader', '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;
const children = this.isSubColumn ? parent.$el.children : parent.$refs.hiddenColumns.children;
const columnIndex = this.getColumnElIndex(children, this.$el);
owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
},
destroyed() {
if (!this.$parent) return;
const parent = this.$parent;
this.owner.store.commit('removeColumn', this.columnConfig, this.isSubColumn ? parent.columnConfig : null);
},
render(h) {
// slots 也要渲染,需要计算合并表头
return h('div', this.$slots.default);
}
};

View File

@@ -0,0 +1,153 @@
import LayoutObserver from './layout-observer';
import { mapStates } from './store/helper';
export default {
name: 'ElTableFooter',
mixins: [LayoutObserver],
render(h) {
let sums = [];
if (this.summaryMethod) {
sums = this.summaryMethod({ columns: this.columns, data: this.store.states.data });
} else {
this.columns.forEach((column, index) => {
if (index === 0) {
sums[index] = this.sumText;
return;
}
const values = this.store.states.data.map(item => Number(item[column.property]));
const precisions = [];
let notNumber = true;
values.forEach(value => {
if (!isNaN(value)) {
notNumber = false;
let decimal = ('' + value).split('.')[1];
precisions.push(decimal ? decimal.length : 0);
}
});
const precision = Math.max.apply(null, precisions);
if (!notNumber) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return parseFloat((prev + curr).toFixed(Math.min(precision, 20)));
} else {
return prev;
}
}, 0);
} else {
sums[index] = '';
}
});
}
return (
<table
class="el-table__footer"
cellspacing="0"
cellpadding="0"
border="0">
<colgroup>
{
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
{
this.hasGutter ? <col name="gutter" /> : ''
}
</colgroup>
<tbody class={ [{ 'has-gutter': this.hasGutter }] }>
<tr>
{
this.columns.map((column, cellIndex) => <td
key={cellIndex}
colspan={ column.colSpan }
rowspan={ column.rowSpan }
class={ this.getRowClasses(column, cellIndex) }>
<div class={ ['cell', column.labelClassName] }>
{
sums[cellIndex]
}
</div>
</td>)
}
{
this.hasGutter ? <th class="gutter"></th> : ''
}
</tr>
</tbody>
</table>
);
},
props: {
fixed: String,
store: {
required: true
},
summaryMethod: Function,
sumText: String,
border: Boolean,
defaultSort: {
type: Object,
default() {
return {
prop: '',
order: ''
};
}
}
},
computed: {
table() {
return this.$parent;
},
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: {
isCellHidden(index, columns, column) {
if (this.fixed === true || this.fixed === 'left') {
return index >= this.leftFixedLeafCount;
} else if (this.fixed === 'right') {
let before = 0;
for (let i = 0; i < index; i++) {
before += columns[i].colSpan;
}
return before < this.columnsCount - this.rightFixedLeafCount;
} else if (!this.fixed && column.fixed) { // hide cell when footer instance is not fixed and column is fixed
return true;
} else {
return (index < this.leftFixedCount) || (index >= this.columnsCount - this.rightFixedCount);
}
},
getRowClasses(column, cellIndex) {
const classes = [column.id, column.align, column.labelClassName];
if (column.className) {
classes.push(column.className);
}
if (this.isCellHidden(cellIndex, this.columns, column)) {
classes.push('is-hidden');
}
if (!column.children) {
classes.push('is-leaf');
}
return classes;
}
}
};

View File

@@ -0,0 +1,510 @@
import Vue from 'vue';
import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom';
import ElCheckbox from 'element-ui/packages/checkbox';
import FilterPanel from './filter-panel.vue';
import LayoutObserver from './layout-observer';
import { mapStates } from './store/helper';
const getAllColumns = (columns) => {
const result = [];
columns.forEach((column) => {
if (column.children) {
result.push(column);
result.push.apply(result, getAllColumns(column.children));
} else {
result.push(column);
}
});
return result;
};
const convertToRows = (originColumns) => {
let maxLevel = 1;
const traverse = (column, parent) => {
if (parent) {
column.level = parent.level + 1;
if (maxLevel < column.level) {
maxLevel = column.level;
}
}
if (column.children) {
let colSpan = 0;
column.children.forEach((subColumn) => {
traverse(subColumn, column);
colSpan += subColumn.colSpan;
});
column.colSpan = colSpan;
} else {
column.colSpan = 1;
}
};
originColumns.forEach((column) => {
column.level = 1;
traverse(column);
});
const rows = [];
for (let i = 0; i < maxLevel; i++) {
rows.push([]);
}
const allColumns = getAllColumns(originColumns);
allColumns.forEach((column) => {
if (!column.children) {
column.rowSpan = maxLevel - column.level + 1;
} else {
column.rowSpan = 1;
}
rows[column.level - 1].push(column);
});
return rows;
};
export default {
name: 'ElTableHeader',
mixins: [LayoutObserver],
render(h) {
const originColumns = this.store.states.originColumns;
const columnRows = convertToRows(originColumns, this.columns);
// 是否拥有多级表头
const isGroup = columnRows.length > 1;
if (isGroup) this.$parent.isGroup = true;
return (
<table
class="el-table__header"
cellspacing="0"
cellpadding="0"
border="0">
<colgroup>
{
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
{
this.hasGutter ? <col name="gutter" /> : ''
}
</colgroup>
<thead class={ [{ 'is-group': isGroup, 'has-gutter': this.hasGutter }] }>
{
this._l(columnRows, (columns, rowIndex) =>
<tr
style={ this.getHeaderRowStyle(rowIndex) }
class={ this.getHeaderRowClass(rowIndex) }
>
{
columns.map((column, cellIndex) => (<th
colspan={ column.colSpan }
rowspan={ column.rowSpan }
on-mousemove={ ($event) => 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 }>
<div class={ ['cell', column.filteredValue && column.filteredValue.length > 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 ? (<span
class="caret-wrapper"
on-click={ ($event) => this.handleSortClick($event, column) }>
<i class="sort-caret ascending"
on-click={ ($event) => this.handleSortClick($event, column, 'ascending') }>
</i>
<i class="sort-caret descending"
on-click={ ($event) => this.handleSortClick($event, column, 'descending') }>
</i>
</span>) : ''
}
{
column.filterable ? (<span
class="el-table__column-filter-trigger"
on-click={ ($event) => this.handleFilterClick($event, column) }>
<i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i>
</span>) : ''
}
</div>
</th>))
}
{
this.hasGutter ? <th class="gutter"></th> : ''
}
</tr>
)
}
</thead>
</table>
);
},
props: {
fixed: String,
store: {
required: true
},
border: Boolean,
defaultSort: {
type: Object,
default() {
return {
prop: '',
order: ''
};
}
}
},
components: {
ElCheckbox
},
computed: {
table() {
return this.$parent;
},
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() {
this.filterPanels = {};
},
mounted() {
// nextTick 是有必要的 https://github.com/ElemeFE/element/pull/11311
this.$nextTick(() => {
const { prop, order } = this.defaultSort;
const init = true;
this.store.commit('sort', { prop, order, init });
});
},
beforeDestroy() {
const panels = this.filterPanels;
for (let prop in panels) {
if (panels.hasOwnProperty(prop) && panels[prop]) {
panels[prop].$destroy(true);
}
}
},
methods: {
isCellHidden(index, columns) {
let start = 0;
for (let i = 0; i < index; i++) {
start += columns[i].colSpan;
}
const after = start + columns[index].colSpan - 1;
if (this.fixed === true || this.fixed === 'left') {
return after >= this.leftFixedLeafCount;
} else if (this.fixed === 'right') {
return start < this.columnsCount - this.rightFixedLeafCount;
} else {
return (after < this.leftFixedLeafCount) || (start >= this.columnsCount - this.rightFixedLeafCount);
}
},
getHeaderRowStyle(rowIndex) {
const headerRowStyle = this.table.headerRowStyle;
if (typeof headerRowStyle === 'function') {
return headerRowStyle.call(null, { rowIndex });
}
return headerRowStyle;
},
getHeaderRowClass(rowIndex) {
const classes = [];
const headerRowClassName = this.table.headerRowClassName;
if (typeof headerRowClassName === 'string') {
classes.push(headerRowClassName);
} else if (typeof headerRowClassName === 'function') {
classes.push(headerRowClassName.call(null, { rowIndex }));
}
return classes.join(' ');
},
getHeaderCellStyle(rowIndex, columnIndex, row, column) {
const headerCellStyle = this.table.headerCellStyle;
if (typeof headerCellStyle === 'function') {
return headerCellStyle.call(null, {
rowIndex,
columnIndex,
row,
column
});
}
return headerCellStyle;
},
getHeaderCellClass(rowIndex, columnIndex, row, column) {
const classes = [column.id, column.order, column.headerAlign, column.className, column.labelClassName];
if (rowIndex === 0 && this.isCellHidden(columnIndex, row)) {
classes.push('is-hidden');
}
if (!column.children) {
classes.push('is-leaf');
}
if (column.sortable) {
classes.push('is-sortable');
}
const headerCellClassName = this.table.headerCellClassName;
if (typeof headerCellClassName === 'string') {
classes.push(headerCellClassName);
} else if (typeof headerCellClassName === 'function') {
classes.push(headerCellClassName.call(null, {
rowIndex,
columnIndex,
row,
column
}));
}
return classes.join(' ');
},
toggleAllSelection(event) {
event.stopPropagation();
this.store.commit('toggleAllSelection');
},
handleFilterClick(event, column) {
event.stopPropagation();
const target = event.target;
let cell = target.tagName === 'TH' ? target : target.parentNode;
if (hasClass(cell, 'noclick')) return;
cell = cell.querySelector('.el-table__column-filter-trigger') || cell;
const table = this.$parent;
let filterPanel = this.filterPanels[column.id];
if (filterPanel && column.filterOpened) {
filterPanel.showPopper = false;
return;
}
if (!filterPanel) {
filterPanel = new Vue(FilterPanel);
this.filterPanels[column.id] = filterPanel;
if (column.filterPlacement) {
filterPanel.placement = column.filterPlacement;
}
filterPanel.table = table;
filterPanel.cell = cell;
filterPanel.column = column;
!this.$isServer && filterPanel.$mount(document.createElement('div'));
}
setTimeout(() => {
filterPanel.showPopper = true;
}, 16);
},
handleHeaderClick(event, column) {
if (!column.filters && column.sortable) {
this.handleSortClick(event, column);
} else if (column.filterable && !column.sortable) {
this.handleFilterClick(event, column);
}
this.$parent.$emit('header-click', column, event);
},
handleHeaderContextMenu(event, column) {
this.$parent.$emit('header-contextmenu', column, event);
},
handleMouseDown(event, column) {
if (this.$isServer) return;
if (column.children && column.children.length > 0) return;
/* istanbul ignore if */
if (this.draggingColumn && this.border) {
this.dragging = true;
this.$parent.resizeProxyVisible = true;
const table = this.$parent;
const tableEl = table.$el;
const tableLeft = tableEl.getBoundingClientRect().left;
const columnEl = this.$el.querySelector(`th.${column.id}`);
const columnRect = columnEl.getBoundingClientRect();
const minLeft = columnRect.left - tableLeft + 30;
addClass(columnEl, 'noclick');
this.dragState = {
startMouseLeft: event.clientX,
startLeft: columnRect.right - tableLeft,
startColumnLeft: columnRect.left - tableLeft,
tableLeft
};
const resizeProxy = table.$refs.resizeProxy;
resizeProxy.style.left = this.dragState.startLeft + 'px';
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; };
const handleMouseMove = (event) => {
const deltaLeft = event.clientX - this.dragState.startMouseLeft;
const proxyLeft = this.dragState.startLeft + deltaLeft;
resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px';
};
const handleMouseUp = () => {
if (this.dragging) {
const {
startColumnLeft,
startLeft
} = this.dragState;
const finalLeft = parseInt(resizeProxy.style.left, 10);
const columnWidth = finalLeft - startColumnLeft;
column.width = column.realWidth = columnWidth;
table.$emit('header-dragend', column.width, startLeft - startColumnLeft, column, event);
this.store.scheduleLayout();
document.body.style.cursor = '';
this.dragging = false;
this.draggingColumn = null;
this.dragState = {};
table.resizeProxyVisible = false;
}
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.onselectstart = null;
document.ondragstart = null;
setTimeout(function() {
removeClass(columnEl, 'noclick');
}, 0);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
},
handleMouseMove(event, column) {
if (column.children && column.children.length > 0) return;
let target = event.target;
while (target && target.tagName !== 'TH') {
target = target.parentNode;
}
if (!column || !column.resizable) return;
if (!this.dragging && this.border) {
let rect = target.getBoundingClientRect();
const bodyStyle = document.body.style;
if (rect.width > 12 && rect.right - event.pageX < 8) {
bodyStyle.cursor = 'col-resize';
if (hasClass(target, 'is-sortable')) {
target.style.cursor = 'col-resize';
}
this.draggingColumn = column;
} else if (!this.dragging) {
bodyStyle.cursor = '';
if (hasClass(target, 'is-sortable')) {
target.style.cursor = 'pointer';
}
this.draggingColumn = null;
}
}
},
handleMouseOut() {
if (this.$isServer) return;
document.body.style.cursor = '';
},
toggleOrder({ order, sortOrders }) {
if (order === '') return sortOrders[0];
const index = sortOrders.indexOf(order || null);
return sortOrders[index > sortOrders.length - 2 ? 0 : index + 1];
},
handleSortClick(event, column, givenOrder) {
event.stopPropagation();
let order = column.order === givenOrder
? null
: (givenOrder || this.toggleOrder(column));
let target = event.target;
while (target && target.tagName !== 'TH') {
target = target.parentNode;
}
if (target && target.tagName === 'TH') {
if (hasClass(target, 'noclick')) {
removeClass(target, 'noclick');
return;
}
}
if (!column.sortable) return;
const states = this.store.states;
let sortProp = states.sortProp;
let sortOrder;
const sortingColumn = states.sortingColumn;
if (sortingColumn !== column || (sortingColumn === column && sortingColumn.order === null)) {
if (sortingColumn) {
sortingColumn.order = null;
}
states.sortingColumn = column;
sortProp = column.property;
}
if (!order) {
sortOrder = column.order = null;
} else {
sortOrder = column.order = order;
}
states.sortProp = sortProp;
states.sortOrder = sortOrder;
this.store.commit('changeSortCondition');
}
},
data() {
return {
draggingColumn: null,
dragging: false,
dragState: {}
};
}
};

View File

@@ -0,0 +1,250 @@
import Vue from 'vue';
import scrollbarWidth from 'element-ui/src/utils/scrollbar-width';
import { parseHeight } from './util';
class TableLayout {
constructor(options) {
this.observers = [];
this.table = null;
this.store = null;
this.columns = null;
this.fit = true;
this.showHeader = true;
this.height = null;
this.scrollX = false;
this.scrollY = false;
this.bodyWidth = null;
this.fixedWidth = null;
this.rightFixedWidth = null;
this.tableHeight = null;
this.headerHeight = 44; // Table Header Height
this.appendHeight = 0; // Append Slot Height
this.footerHeight = 44; // Table Footer Height
this.viewportHeight = null; // Table Height - Scroll Bar Height
this.bodyHeight = null; // Table Height - Table Header Height
this.fixedBodyHeight = null; // Table Height - Table Header Height - Scroll Bar Height
this.gutterWidth = scrollbarWidth();
for (let name in options) {
if (options.hasOwnProperty(name)) {
this[name] = options[name];
}
}
if (!this.table) {
throw new Error('table is required for Table Layout');
}
if (!this.store) {
throw new Error('store is required for Table Layout');
}
}
updateScrollY() {
const height = this.height;
if (height === null) return false;
const bodyWrapper = this.table.bodyWrapper;
if (this.table.$el && bodyWrapper) {
const body = bodyWrapper.querySelector('.el-table__body');
const prevScrollY = this.scrollY;
const scrollY = body.offsetHeight > this.bodyHeight;
this.scrollY = scrollY;
return prevScrollY !== scrollY;
}
return false;
}
setHeight(value, prop = 'height') {
if (Vue.prototype.$isServer) return;
const el = this.table.$el;
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;
this.updateElsHeight();
}
}
setMaxHeight(value) {
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() {
if (!this.table.$ready) return Vue.nextTick(() => this.updateElsHeight());
const { headerWrapper, appendWrapper, footerWrapper } = this.table.$refs;
this.appendHeight = appendWrapper ? appendWrapper.offsetHeight : 0;
if (this.showHeader && !headerWrapper) return;
// fix issue (https://github.com/ElemeFE/element/pull/16956)
const headerTrElm = headerWrapper ? headerWrapper.querySelector('.el-table__header tr') : null;
const noneHeader = this.headerDisplayNone(headerTrElm);
const headerHeight = this.headerHeight = !this.showHeader ? 0 : headerWrapper.offsetHeight;
if (this.showHeader && !noneHeader && headerWrapper.offsetWidth > 0 && (this.table.columns || []).length > 0 && headerHeight < 2) {
return Vue.nextTick(() => this.updateElsHeight());
}
const tableHeight = this.tableHeight = this.table.$el.clientHeight;
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;
const noData = !(this.store.states.data && this.store.states.data.length);
this.viewportHeight = this.scrollX ? tableHeight - (noData ? 0 : this.gutterWidth) : tableHeight;
this.updateScrollY();
this.notifyObservers('scrollable');
}
headerDisplayNone(elm) {
if (!elm) return true;
let headerChild = elm;
while (headerChild.tagName !== 'DIV') {
if (getComputedStyle(headerChild).display === 'none') {
return true;
}
headerChild = headerChild.parentElement;
}
return false;
}
updateColumnsWidth() {
if (Vue.prototype.$isServer) return;
const fit = this.fit;
const bodyWidth = this.table.$el.clientWidth;
let bodyMinWidth = 0;
const flattenColumns = this.getFlattenColumns();
let flexColumns = flattenColumns.filter((column) => typeof column.width !== 'number');
flattenColumns.forEach((column) => { // Clean those columns whose width changed from flex to unflex
if (typeof column.width === 'number' && column.realWidth) column.realWidth = null;
});
if (flexColumns.length > 0 && fit) {
flattenColumns.forEach((column) => {
bodyMinWidth += column.width || column.minWidth || 80;
});
const scrollYWidth = this.scrollY ? this.gutterWidth : 0;
if (bodyMinWidth <= bodyWidth - scrollYWidth) { // DON'T HAVE SCROLL BAR
this.scrollX = false;
const totalFlexWidth = bodyWidth - scrollYWidth - bodyMinWidth;
if (flexColumns.length === 1) {
flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth;
} else {
const allColumnsWidth = flexColumns.reduce((prev, column) => prev + (column.minWidth || 80), 0);
const flexWidthPerPixel = totalFlexWidth / allColumnsWidth;
let noneFirstWidth = 0;
flexColumns.forEach((column, index) => {
if (index === 0) return;
const flexWidth = Math.floor((column.minWidth || 80) * flexWidthPerPixel);
noneFirstWidth += flexWidth;
column.realWidth = (column.minWidth || 80) + flexWidth;
});
flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth - noneFirstWidth;
}
} else { // HAVE HORIZONTAL SCROLL BAR
this.scrollX = true;
flexColumns.forEach(function(column) {
column.realWidth = column.minWidth;
});
}
this.bodyWidth = Math.max(bodyMinWidth, bodyWidth);
this.table.resizeState.width = this.bodyWidth;
} else {
flattenColumns.forEach((column) => {
if (!column.width && !column.minWidth) {
column.realWidth = 80;
} else {
column.realWidth = column.width || column.minWidth;
}
bodyMinWidth += column.realWidth;
});
this.scrollX = bodyMinWidth > bodyWidth;
this.bodyWidth = bodyMinWidth;
}
const fixedColumns = this.store.states.fixedColumns;
if (fixedColumns.length > 0) {
let fixedWidth = 0;
fixedColumns.forEach(function(column) {
fixedWidth += column.realWidth || column.width;
});
this.fixedWidth = fixedWidth;
}
const rightFixedColumns = this.store.states.rightFixedColumns;
if (rightFixedColumns.length > 0) {
let rightFixedWidth = 0;
rightFixedColumns.forEach(function(column) {
rightFixedWidth += column.realWidth || column.width;
});
this.rightFixedWidth = rightFixedWidth;
}
this.notifyObservers('columns');
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notifyObservers(event) {
const observers = this.observers;
observers.forEach((observer) => {
switch (event) {
case 'columns':
observer.onColumnsChange(this);
break;
case 'scrollable':
observer.onScrollableChange(this);
break;
default:
throw new Error(`Table Layout don't have event ${event}.`);
}
});
}
}
export default TableLayout;

View File

@@ -0,0 +1,694 @@
<template>
<div class="el-table"
:class="[{
'el-table--fit': fit,
'el-table--striped': stripe,
'el-table--border': border || isGroup,
'el-table--hidden': isHidden,
'el-table--group': isGroup,
'el-table--fluid-height': maxHeight,
'el-table--scrollable-x': layout.scrollX,
'el-table--scrollable-y': layout.scrollY,
'el-table--enable-row-hover': !store.states.isComplex,
'el-table--enable-row-transition': (store.states.data || []).length !== 0 && (store.states.data || []).length < 100
}, tableSize ? `el-table--${ tableSize }` : '']"
@mouseleave="handleMouseLeave($event)">
<div class="hidden-columns" ref="hiddenColumns"><slot></slot></div>
<div
v-if="showHeader"
v-mousewheel="handleHeaderFooterMousewheel"
class="el-table__header-wrapper"
ref="headerWrapper">
<table-header
ref="tableHeader"
:store="store"
:border="border"
:default-sort="defaultSort"
:style="{
width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
}">
</table-header>
</div>
<div
class="el-table__body-wrapper"
ref="bodyWrapper"
:class="[layout.scrollX ? `is-scrolling-${scrollPosition}` : 'is-scrolling-none']"
:style="[bodyHeight]">
<table-body
:context="context"
:store="store"
:stripe="stripe"
:row-class-name="rowClassName"
:row-style="rowStyle"
:highlight="highlightCurrentRow"
:style="{
width: bodyWidth
}">
</table-body>
<div
v-if="!data || data.length === 0"
class="el-table__empty-block"
ref="emptyBlock"
:style="emptyBlockStyle">
<span class="el-table__empty-text" >
<slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot>
</span>
</div>
<div
v-if="$slots.append"
class="el-table__append-wrapper"
ref="appendWrapper">
<slot name="append"></slot>
</div>
</div>
<div
v-if="showSummary"
v-show="data && data.length > 0"
v-mousewheel="handleHeaderFooterMousewheel"
class="el-table__footer-wrapper"
ref="footerWrapper">
<table-footer
:store="store"
:border="border"
:sum-text="sumText || t('el.table.sumText')"
:summary-method="summaryMethod"
:default-sort="defaultSort"
:style="{
width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
}">
</table-footer>
</div>
<div
v-if="fixedColumns.length > 0"
v-mousewheel="handleFixedMousewheel"
class="el-table__fixed"
ref="fixedWrapper"
:style="[{
width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
},
fixedHeight]">
<div
v-if="showHeader"
class="el-table__fixed-header-wrapper"
ref="fixedHeaderWrapper" >
<table-header
ref="fixedTableHeader"
fixed="left"
:border="border"
:store="store"
:style="{
width: bodyWidth
}"></table-header>
</div>
<div
class="el-table__fixed-body-wrapper"
ref="fixedBodyWrapper"
:style="[{
top: layout.headerHeight + 'px'
},
fixedBodyHeight]">
<table-body
fixed="left"
:store="store"
:stripe="stripe"
:highlight="highlightCurrentRow"
:row-class-name="rowClassName"
:row-style="rowStyle"
:style="{
width: bodyWidth
}">
</table-body>
<div
v-if="$slots.append"
class="el-table__append-gutter"
:style="{ height: layout.appendHeight + 'px'}"></div>
</div>
<div
v-if="showSummary"
v-show="data && data.length > 0"
class="el-table__fixed-footer-wrapper"
ref="fixedFooterWrapper">
<table-footer
fixed="left"
:border="border"
:sum-text="sumText || t('el.table.sumText')"
:summary-method="summaryMethod"
:store="store"
:style="{
width: bodyWidth
}"></table-footer>
</div>
</div>
<div
v-if="rightFixedColumns.length > 0"
v-mousewheel="handleFixedMousewheel"
class="el-table__fixed-right"
ref="rightFixedWrapper"
:style="[{
width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '',
right: layout.scrollY ? (border ? layout.gutterWidth : (layout.gutterWidth || 0)) + 'px' : ''
},
fixedHeight]">
<div v-if="showHeader"
class="el-table__fixed-header-wrapper"
ref="rightFixedHeaderWrapper">
<table-header
ref="rightFixedTableHeader"
fixed="right"
:border="border"
:store="store"
:style="{
width: bodyWidth
}"></table-header>
</div>
<div
class="el-table__fixed-body-wrapper"
ref="rightFixedBodyWrapper"
:style="[{
top: layout.headerHeight + 'px'
},
fixedBodyHeight]">
<table-body
fixed="right"
:store="store"
:stripe="stripe"
:row-class-name="rowClassName"
:row-style="rowStyle"
:highlight="highlightCurrentRow"
:style="{
width: bodyWidth
}">
</table-body>
<div
v-if="$slots.append"
class="el-table__append-gutter"
:style="{ height: layout.appendHeight + 'px' }"></div>
</div>
<div
v-if="showSummary"
v-show="data && data.length > 0"
class="el-table__fixed-footer-wrapper"
ref="rightFixedFooterWrapper">
<table-footer
fixed="right"
:border="border"
:sum-text="sumText || t('el.table.sumText')"
:summary-method="summaryMethod"
:store="store"
:style="{
width: bodyWidth
}"></table-footer>
</div>
</div>
<div
v-if="rightFixedColumns.length > 0"
class="el-table__fixed-right-patch"
ref="rightFixedPatch"
:style="{
width: layout.scrollY ? layout.gutterWidth + 'px' : '0',
height: layout.headerHeight + 'px'
}"></div>
<div class="el-table__column-resize-proxy" ref="resizeProxy" v-show="resizeProxyVisible"></div>
</div>
</template>
<script type="text/babel">
import ElCheckbox from 'element-ui/packages/checkbox';
import { debounce, throttle } from 'throttle-debounce';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
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, mapStates } from './store/helper';
import TableLayout from './table-layout';
import TableBody from './table-body';
import TableHeader from './table-header';
import TableFooter from './table-footer';
import { parseHeight } from './util';
let tableIdSeed = 1;
export default {
name: 'ElTable',
mixins: [Locale, Migrating],
directives: {
Mousewheel
},
props: {
data: {
type: Array,
default: function() {
return [];
}
},
size: String,
width: [String, Number],
height: [String, Number],
maxHeight: [String, Number],
fit: {
type: Boolean,
default: true
},
stripe: Boolean,
border: Boolean,
rowKey: [String, Function],
context: {},
showHeader: {
type: Boolean,
default: true
},
showSummary: Boolean,
sumText: String,
summaryMethod: Function,
rowClassName: [String, Function],
rowStyle: [Object, Function],
cellClassName: [String, Function],
cellStyle: [Object, Function],
headerRowClassName: [String, Function],
headerRowStyle: [Object, Function],
headerCellClassName: [String, Function],
headerCellStyle: [Object, Function],
highlightCurrentRow: Boolean,
currentRowKey: [String, Number],
emptyText: String,
expandRowKeys: Array,
defaultExpandAll: Boolean,
defaultSort: Object,
tooltipEffect: String,
spanMethod: Function,
selectOnIndeterminate: {
type: Boolean,
default: true
},
indent: {
type: Number,
default: 16
},
treeProps: {
type: Object,
default() {
return {
hasChildren: 'hasChildren',
children: 'children'
};
}
},
lazy: Boolean,
load: Function
},
components: {
TableHeader,
TableFooter,
TableBody,
ElCheckbox
},
methods: {
getMigratingConfig() {
return {
events: {
expand: 'expand is renamed to expand-change'
}
};
},
setCurrentRow(row) {
this.store.commit('setCurrentRow', row);
},
toggleRowSelection(row, selected) {
this.store.toggleRowSelection(row, selected, false);
this.store.updateAllSelected();
},
toggleRowExpansion(row, expanded) {
this.store.toggleRowExpansionAdapter(row, expanded);
},
clearSelection() {
this.store.clearSelection();
},
clearFilter(columnKeys) {
this.store.clearFilter(columnKeys);
},
clearSort() {
this.store.clearSort();
},
handleMouseLeave() {
this.store.commit('setHoverRow', null);
if (this.hoverState) this.hoverState = null;
},
updateScrollY() {
const changed = this.layout.updateScrollY();
if (changed) {
this.layout.notifyObservers('scrollable');
this.layout.updateColumnsWidth();
}
},
handleFixedMousewheel(event, data) {
const bodyWrapper = this.bodyWrapper;
if (Math.abs(data.spinY) > 0) {
const currentScrollTop = bodyWrapper.scrollTop;
if (data.pixelY < 0 && currentScrollTop !== 0) {
event.preventDefault();
}
if (data.pixelY > 0 && bodyWrapper.scrollHeight - bodyWrapper.clientHeight > currentScrollTop) {
event.preventDefault();
}
bodyWrapper.scrollTop += Math.ceil(data.pixelY / 5);
} else {
bodyWrapper.scrollLeft += Math.ceil(data.pixelX / 5);
}
},
handleHeaderFooterMousewheel(event, data) {
const { pixelX, pixelY } = data;
if (Math.abs(pixelX) >= Math.abs(pixelY)) {
this.bodyWrapper.scrollLeft += data.pixelX / 5;
}
},
// TODO 使用 CSS transform
syncPostion: throttle(20, function() {
const { scrollLeft, scrollTop, offsetWidth, scrollWidth } = this.bodyWrapper;
const { headerWrapper, footerWrapper, fixedBodyWrapper, rightFixedBodyWrapper } = this.$refs;
if (headerWrapper) headerWrapper.scrollLeft = scrollLeft;
if (footerWrapper) footerWrapper.scrollLeft = scrollLeft;
if (fixedBodyWrapper) fixedBodyWrapper.scrollTop = scrollTop;
if (rightFixedBodyWrapper) rightFixedBodyWrapper.scrollTop = scrollTop;
const maxScrollLeftPosition = scrollWidth - offsetWidth - 1;
if (scrollLeft >= maxScrollLeftPosition) {
this.scrollPosition = 'right';
} else if (scrollLeft === 0) {
this.scrollPosition = 'left';
} else {
this.scrollPosition = 'middle';
}
}),
bindEvents() {
this.bodyWrapper.addEventListener('scroll', this.syncPostion, { passive: true });
if (this.fit) {
addResizeListener(this.$el, this.resizeListener);
}
},
unbindEvents() {
this.bodyWrapper.removeEventListener('scroll', this.syncPostion, { passive: true });
if (this.fit) {
removeResizeListener(this.$el, this.resizeListener);
}
},
resizeListener() {
if (!this.$ready) return;
let shouldUpdateLayout = false;
const el = this.$el;
const { width: oldWidth, height: oldHeight } = this.resizeState;
const width = el.offsetWidth;
if (oldWidth !== width) {
shouldUpdateLayout = true;
}
const height = el.offsetHeight;
if ((this.height || this.shouldUpdateHeight) && oldHeight !== height) {
shouldUpdateLayout = true;
}
if (shouldUpdateLayout) {
this.resizeState.width = width;
this.resizeState.height = height;
this.doLayout();
}
},
doLayout() {
if (this.shouldUpdateHeight) {
this.layout.updateElsHeight();
}
this.layout.updateColumnsWidth();
},
sort(prop, order) {
this.store.commit('sort', { prop, order });
},
toggleAllSelection() {
this.store.commit('toggleAllSelection');
}
},
computed: {
tableSize() {
return this.size || (this.$ELEMENT || {}).size;
},
bodyWrapper() {
return this.$refs.bodyWrapper;
},
shouldUpdateHeight() {
return this.height ||
this.maxHeight ||
this.fixedColumns.length > 0 ||
this.rightFixedColumns.length > 0;
},
bodyWidth() {
const { bodyWidth, scrollY, gutterWidth } = this.layout;
return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
},
bodyHeight() {
const { headerHeight = 0, bodyHeight, footerHeight = 0} = this.layout;
if (this.height) {
return {
height: bodyHeight ? bodyHeight + 'px' : ''
};
} else if (this.maxHeight) {
const maxHeight = parseHeight(this.maxHeight);
if (typeof maxHeight === 'number') {
return {
'max-height': (maxHeight - footerHeight - (this.showHeader ? headerHeight : 0)) + 'px'
};
}
}
return {};
},
fixedBodyHeight() {
if (this.height) {
return {
height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : ''
};
} else if (this.maxHeight) {
let maxHeight = parseHeight(this.maxHeight);
if (typeof maxHeight === 'number') {
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'
};
}
}
return {};
},
fixedHeight() {
if (this.maxHeight) {
if (this.showSummary) {
return {
bottom: 0
};
}
return {
bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
};
} else {
if (this.showSummary) {
return {
height: this.layout.tableHeight ? this.layout.tableHeight + 'px' : ''
};
}
return {
height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
};
}
},
emptyBlockStyle() {
if (this.data && this.data.length) return null;
let height = '100%';
if (this.layout.appendHeight) {
height = `calc(100% - ${this.layout.appendHeight}px)`;
}
return {
width: this.bodyWidth,
height
};
},
...mapStates({
selection: 'selection',
columns: 'columns',
tableData: 'data',
fixedColumns: 'fixedColumns',
rightFixedColumns: 'rightFixedColumns'
})
},
watch: {
height: {
immediate: true,
handler(value) {
this.layout.setHeight(value);
}
},
maxHeight: {
immediate: true,
handler(value) {
this.layout.setMaxHeight(value);
}
},
currentRowKey: {
immediate: true,
handler(value) {
if (!this.rowKey) return;
this.store.setCurrentRowKey(value);
}
},
data: {
immediate: true,
handler(value) {
this.store.commit('setData', value);
}
},
expandRowKeys: {
immediate: true,
handler(newVal) {
if (newVal) {
this.store.setExpandRowKeysAdapter(newVal);
}
}
}
},
created() {
this.tableId = 'el-table_' + tableIdSeed++;
this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
},
mounted() {
this.bindEvents();
this.store.updateColumns();
this.doLayout();
this.resizeState = {
width: this.$el.offsetWidth,
height: this.$el.offsetHeight
};
// init filters
this.store.states.columns.forEach(column => {
if (column.filteredValue && column.filteredValue.length) {
this.store.commit('filterChange', {
column,
values: column.filteredValue,
silent: true
});
}
});
this.$ready = true;
},
destroyed() {
this.unbindEvents();
},
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,
lazyColumnIdentifier: hasChildren,
childrenColumnName: children
});
const layout = new TableLayout({
store: this.store,
table: this,
fit: this.fit,
showHeader: this.showHeader
});
return {
layout,
isHidden: false,
renderExpanded: null,
resizeProxyVisible: false,
resizeState: {
width: null,
height: null
},
// 是否拥有多级表头
isGroup: false,
scrollPosition: 'left'
};
}
};
</script>

255
packages/table/src/util.js Normal file
View File

@@ -0,0 +1,255 @@
import { getValueByPath } from 'element-ui/src/utils/util';
export const getCell = function(event) {
let cell = event.target;
while (cell && cell.tagName.toUpperCase() !== 'HTML') {
if (cell.tagName.toUpperCase() === 'TD') {
return cell;
}
cell = cell.parentNode;
}
return null;
};
const isObject = function(obj) {
return obj !== null && typeof obj === 'object';
};
export const orderBy = function(array, sortKey, reverse, sortMethod, sortBy) {
if (!sortKey && !sortMethod && (!sortBy || Array.isArray(sortBy) && !sortBy.length)) {
return array;
}
if (typeof reverse === 'string') {
reverse = reverse === 'descending' ? -1 : 1;
} else {
reverse = (reverse && reverse < 0) ? -1 : 1;
}
const getKey = sortMethod ? null : function(value, index) {
if (sortBy) {
if (!Array.isArray(sortBy)) {
sortBy = [sortBy];
}
return sortBy.map(function(by) {
if (typeof by === 'string') {
return getValueByPath(value, by);
} else {
return by(value, index, array);
}
});
}
if (sortKey !== '$key') {
if (isObject(value) && '$value' in value) value = value.$value;
}
return [isObject(value) ? getValueByPath(value, sortKey) : value];
};
const compare = function(a, b) {
if (sortMethod) {
return sortMethod(a.value, b.value);
}
for (let i = 0, len = a.key.length; i < len; i++) {
if (a.key[i] < b.key[i]) {
return -1;
}
if (a.key[i] > b.key[i]) {
return 1;
}
}
return 0;
};
return array.map(function(value, index) {
return {
value: value,
index: index,
key: getKey ? getKey(value, index) : null
};
}).sort(function(a, b) {
let order = compare(a, b);
if (!order) {
// make stable https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
order = a.index - b.index;
}
return order * reverse;
}).map(item => item.value);
};
export const getColumnById = function(table, columnId) {
let column = null;
table.columns.forEach(function(item) {
if (item.id === columnId) {
column = item;
}
});
return column;
};
export const getColumnByKey = function(table, columnKey) {
let column = null;
for (let i = 0; i < table.columns.length; i++) {
const item = table.columns[i];
if (item.columnKey === columnKey) {
column = item;
break;
}
}
return column;
};
export const getColumnByCell = function(table, cell) {
const matches = (cell.className || '').match(/el-table_[^\s]+/gm);
if (matches) {
return getColumnById(table, matches[0]);
}
return null;
};
export const getRowIdentity = (row, rowKey) => {
if (!row) throw new Error('row is required when get row identity');
if (typeof rowKey === 'string') {
if (rowKey.indexOf('.') < 0) {
return row[rowKey];
}
let key = rowKey.split('.');
let current = row;
for (let i = 0; i < key.length; i++) {
current = current[key[i]];
}
return current;
} else if (typeof rowKey === 'function') {
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;
};
export function parseHeight(height) {
if (typeof height === 'number') {
return height;
}
if (typeof height === 'string') {
if (/^\d+(?:px)?$/.test(height)) {
return parseInt(height, 10);
} else {
return height;
}
}
return null;
}
// 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 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, null, level + 1);
return;
}
const children = item[childrenKey];
if (!isNil(children)) {
_walker(item, children, level + 1);
}
});
}
root.forEach(item => {
if (item[lazyKey]) {
cb(item, null, 0);
return;
}
const children = item[childrenKey];
if (!isNil(children)) {
_walker(item, children, 0);
}
});
}