Table: add new features for Table and optimize code (#15709)

pull/15798/head
hetech 2019-05-30 19:33:30 +08:00 committed by luckyCao
parent 99d613fc05
commit f6bab114e3
22 changed files with 2055 additions and 1793 deletions

View File

@ -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
<template>
@ -1340,11 +1340,12 @@ When the row content is too long and you do not want to display the horizontal s
<el-table
:data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
row-key="id">
default-expand-all>
<el-table-column
prop="date"
label="Date"
label="date"
sortable
width="180">
</el-table-column>
@ -1363,7 +1364,7 @@ When the row content is too long and you do not want to display the horizontal s
border
lazy
:load="load"
>
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="date"
label="Date"
@ -1429,17 +1430,19 @@ When the row content is too long and you do not want to display the horizontal s
},
methods: {
load(tree, treeNode, resolve) {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
setTimeout(() => {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
}, 1000)
}
},
}
@ -1825,7 +1828,7 @@ You can customize row index in `type=index` columns.
| header-cell-style | function that returns custom style for a cell in table header, or an object assigning custom style for every cell in table header | Function({row, column, rowIndex, columnIndex})/Object | — | — |
| row-key | key of row data, used for optimizing rendering. Required if `reserve-selection` is on or display tree data. When its type is String, multi-level access is supported, e.g. `user.info.id`, but `user.info[0].id` is not supported, in which case `Function` should be used. | Function(row)/String | — | — |
| empty-text | Displayed text when data is empty. You can customize this area with `slot="empty"` | String | — | No Data |
| default-expand-all | whether expand all rows by default, only works when the table has a column type="expand" | Boolean | — | false |
| default-expand-all | whether expand all rows by default, works when the table has a column type="expand" or contains tree structure data | Boolean | — | false |
| expand-row-keys | set expanded rows by this prop, prop's value is the keys of expand rows, you should set row-key before using this prop | Array | — | |
| default-sort | set the default sort column and order. property `prop` is used to set default sort column, property `order` is used to set default sort order | Object | `order`: ascending, descending | if `prop` is set, and `order` is not set, then `order` is default to ascending |
| tooltip-effect | tooltip `effect` property | String | dark/light | | dark |
@ -1836,7 +1839,8 @@ You can customize row index in `type=index` columns.
| select-on-indeterminate | controls the behavior of master checkbox in multi-select tables when only some rows are selected (but not all). If true, all rows will be selected, else deselected. | Boolean | — | true |
| indent | horizontal indentation of tree data | Number | — | 16 |
| lazy | whether to lazy loading data | Boolean| — | — |
| load | method for loading child row data, only works when `lazy` is true | Function({ row, treeNode, resolve }) | — | — |
| load | method for loading child row data, only works when `lazy` is true | Function(row, treeNode, resolve) | — | — |
| tree-props | configuration for rendering nested data| Object | — | { hasChildren: 'hasChildren', children: 'children' } |
### Table Events
| Event Name | Description | Parameters |
@ -1857,7 +1861,7 @@ You can customize row index in `type=index` columns.
| filter-change | column's key. If you need to use the filter-change event, this attribute is mandatory to identify which column is being filtered | filters |
| current-change | triggers when current row changes | currentRow, oldCurrentRow |
| header-dragend | triggers after changing a column's width by dragging the column header's border | newWidth, oldWidth, column, event |
| expand-change | triggers when user expands or collapses a row | row, expandedRows |
| expand-change | triggers when user expands or collapses a row (for expandable table, second param is expandedRows; for tree Table, second param is expanded) | row, (expandedRows \| expanded) |
### Table Methods
| Method | Description | Parameters |
@ -1865,7 +1869,7 @@ You can customize row index in `type=index` columns.
| clearSelection | used in multiple selection Table, clear user selection | — |
| toggleRowSelection | used in multiple selection Table, toggle if a certain row is selected. With the second parameter, you can directly set if this row is selected | row, selected |
| toggleAllSelection | used in multiple selection Table, toggle the selected state of all rows | - |
| toggleRowExpansion | used in expandable Table, toggle if a certain row is expanded. With the second parameter, you can directly set if this row is expanded or collapsed | row, expanded |
| toggleRowExpansion | used in expandable Table or tree Table, toggle if a certain row is expanded. With the second parameter, you can directly set if this row is expanded or collapsed | row, expanded |
| setCurrentRow | used in single selection Table, set a certain row selected. If called without any parameter, it will clear selection. | row |
| clearSort | clear sorting, restore data to the original order | — |
| clearFilter | clear filters of the columns whose `columnKey` are passed in. If no params, clear all filters | columnKeys |

View File

@ -1334,7 +1334,7 @@ Cuando el contenido de la fila es demasiado largo y busca no mostrar la barra de
### Datos tree y modo lazy
:::demo Puede mostrar la estructura de datos tipo tree。Cuando se usa, la prop`row-key` es requerida。Entonces, los datos de las filas de los hijos pueden ser cargados asincrónicamente. Poner la propiedad `lazy` de Table a true y la función `load`. Especifique el atributo `hasChildren` en la fila para determinar qué fila contiene hijos.
:::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
<template>
@ -1342,8 +1342,9 @@ Cuando el contenido de la fila es demasiado largo y busca no mostrar la barra de
<el-table
:data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
row-key="id">
default-expand-all>
<el-table-column
prop="date"
label="日期"
@ -1365,7 +1366,7 @@ Cuando el contenido de la fila es demasiado largo y busca no mostrar la barra de
border
lazy
:load="load"
>
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="date"
label="date"
@ -1431,17 +1432,19 @@ Cuando el contenido de la fila es demasiado largo y busca no mostrar la barra de
},
methods: {
load(tree, treeNode, resolve) {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
setTimeout(() => {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
}, 1000)
}
},
}
@ -1841,7 +1844,8 @@ Puede personalizar el índice de la fila con la propiedad `type=index` de las co
| select-on-indeterminate | controla el comportamiento del checkbox maestro en tablas de selección múltiple cuando sólo se seleccionan algunas filas (pero no todas). Si es true, todas las filas serán seleccionadas, de lo contrario deseleccionadas. | Boolean | — | true |
| indent | indentación horizontal de los datos en formato tree | Number | — | 16 |
| lazy | si se realiza un lazy loading de los datos | Boolean | — | — |
| load | metodo para cargar las filas de los hijos, solamente funciona cuando `lazy`es true | Function({ row, treeNode, resolve }) | — | — |
| load | metodo para cargar las filas de los hijos, solamente funciona cuando `lazy`es true | Function(row, treeNode, resolve) | — | — |
| tree-props | configuration for rendering nested data | Object | — | { hasChildren: 'hasChildren', children: 'children' } |
### Eventos de la tabla
| Nombre del evento | Descripción | Parámetros |
@ -1862,7 +1866,7 @@ Puede personalizar el índice de la fila con la propiedad `type=index` de las co
| filter-change | clave de la columna. Si necesitas utilizar el evento filter-change, este atributo es obligatorio para identificar cuál columna está siendo filtrada | filters |
| current-change | se dispara cuando la fila actual cambia | currentRow, oldCurrentRow |
| header-dragend | se dispara después de modificar el ancho de una columna arrastrando el borde de la cabecera. | newWidth, oldWidth, column, event |
| expand-change | se dispara cuando el usuario expande o colapsa una fila | row, expandedRows |
| expand-change | triggers when user expands or collapses a row (for expandable table, second param is expandedRows; for tree Table, second param is expanded) | row, (expandedRows \| expanded) |
### Métodos de la tabla
| Metodo | Descripción | Parametros |
@ -1870,7 +1874,7 @@ Puede personalizar el índice de la fila con la propiedad `type=index` de las co
| clearSelection | utilizado en selección múltiple de la tabla, limpiar selección | — |
| toggleRowSelection | utilizado en selección múltiple de la tabla, alterna si una cierta fila es seleccionada. Con el segundo parámetro, puede directamente establecer si la fila es seleccionada | row, selected |
| toggleAllSelection | usado en Table de seleccion multiple, cambia los estados de seleccion de todas las filas. | - |
| toggleRowExpansion | utilizado en tabla expandible, alterna si una cierta fila es expandida. Con el segundo parámetro, puede directamente establecer si esta fila es expandida o colapsada | row, expanded |
| toggleRowExpansion | used in expandable Table or tree Table, toggle if a certain row is expanded. With the second parameter, you can directly set if this row is expanded or collapsed | row, expanded |
| setCurrentRow | utilizado en tabla con selección sencilla, establece una cierta fila seleccionada. Si es llamado sin ningún parámetro, este puede limpiar la selección | row |
| clearSort | limpiar ordenamiento, restaurar datos a orden original | — |
| clearFilter | Se utiliza para borrar todas las condiciones de filtro cuando no se pasan parámetros, los datos se restaurarán a un estado sin filtrar, o se puede pasar una matriz de columnas para borrar las condiciones de filtro de la columna especificada. | columnKey |

View File

@ -1335,7 +1335,7 @@ Lorsque le contenu d'une ligne est trop long et que vous ne souhaitez pas affich
### Arborescence et lazy loading
:::demo Vous pouvez afficher les données en arborescence, la propriété `row-key` devenant dans ce cas obligatoire. Les données enfants peuvent aussi être chargées de manière asynchrone. Mettez la propriété `lazy` à `true` and utilisez la fonction `load`. Spécifiez l'attribut `hasChildren` pour déterminer quelle ligne contient les enfants.
:::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
<template>
@ -1343,11 +1343,12 @@ Lorsque le contenu d'une ligne est trop long et que vous ne souhaitez pas affich
<el-table
:data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
row-key="id">
default-expand-all>
<el-table-column
prop="date"
label="Date"
label="date"
sortable
width="180">
</el-table-column>
@ -1366,7 +1367,7 @@ Lorsque le contenu d'une ligne est trop long et que vous ne souhaitez pas affich
border
lazy
:load="load"
>
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="date"
label="Date"
@ -1432,17 +1433,19 @@ Lorsque le contenu d'une ligne est trop long et que vous ne souhaitez pas affich
},
methods: {
load(tree, treeNode, resolve) {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
setTimeout(() => {
resolve([
{
id: 31,
date: '2016-05-01',
name: 'wangxiaohu'
}, {
id: 32,
date: '2016-05-01',
name: 'wangxiaohu'
}
])
}, 1000)
}
},
}
@ -1832,7 +1835,7 @@ Vous pouvez personnaliser les indices des colonnes de type `index`.
| header-cell-style | Fonction qui retourne un style pour chaque cellule de header. Peut aussi être un objet assignant un style à chaque cellule de header. | Function({row, column, rowIndex, columnIndex})/Object | — | — |
| row-key | Clé de chaque ligne, utilisée pour optimiser le rendu. Requise si `reserve-selection` est activé. Quand c'est un `String`, l'accès multi-niveaux est supporté, e.g. `user.info.id`, mais `user.info[0].id` n'est pas supporté. Dans ce dernier cas une `Function` devrait être utilisée. | Function(row)/String | — | — |
| empty-text | Texte à afficher quand il n'y a pas de données. Vous pouvez changer cette zone grâce à `slot="empty"`. | String | — | No Data |
| default-expand-all | Si toutes les lignes sont étendues par défaut, ne marche que si des lignes ont type="expand". | Boolean | — | false |
| default-expand-all | whether expand all rows by default, works when the table has a column type="expand" or contains tree structure data | Boolean | — | false |
| expand-row-keys | Détermine les lignes qui sont étendues, contient les clés des lignes correspondantes. Vous devriez configurer `row-key` avant celle-ci. | Array | — | |
| default-sort | Détermine l'ordre de tri par défaut. La propriété `prop` détermine la colonne par défaut, `order` détermine l'ordre par défaut. | Object | `order`: ascending, descending | Si `order` est absent, son défaut sera `ascending`. |
| tooltip-effect | Propriété `effect` de Tooltip. | String | dark/light | | dark |
@ -1841,9 +1844,10 @@ Vous pouvez personnaliser les indices des colonnes de type `index`.
| summary-method | La méthode pour calculer la somme. | Function({ columns, data }) | — | — |
| span-method | Méthode qui retourne les valeurs de colspan et rowspan. | Function({ row, column, rowIndex, columnIndex }) | — | — |
| select-on-indeterminate | Contrôle le comportement de la checkbox globale dans les tables avec sélection multiple lorsque seulement certaines lignes sont sélectionnées. Si `true`, toutes les lignes sont sélectionnées. | Boolean | — | true |
| indent | Indentation horizontale de l'arborescence. | Number | — | 16 |
| lazy | Si le lazy loading doit être utilisé. | Boolean | — | — |
| load | Méthode a utiliser pour le lazy loading, ne fonctionne que lorsque `lazy` est `true`. | Function({ row, treeNode, resolve }) | — | — |
| indent | horizontal indentation of tree data | Number | — | 16 |
| lazy | whether to lazy loading data | Boolean | — | — |
| load | method for loading child row data, only works when `lazy` is true | Function({ row, treeNode, resolve }) | — | — |
| tree-props | configuration for rendering nested data | Object | — | { hasChildren: 'hasChildren', children: 'children' } |
### Évènements de Table
@ -1865,7 +1869,7 @@ Vous pouvez personnaliser les indices des colonnes de type `index`.
| filter-change | column's key. If you need to use the filter-change event, this attribute is mandatory to identify which column is being filtered. | filters |
| current-change | Se déclenche quand la ligne sélectionnée change. | currentRow, oldCurrentRow |
| header-dragend | Se déclenche après un changement de taille de colonne en déplaçant la ligne verticale du header. | newWidth, oldWidth, column, event |
| expand-change | Se déclenche quand l'utilisateur étend ou réduit une ligne. | row, expandedRows |
| expand-change | triggers when user expands or collapses a row (for expandable table, second param is expandedRows; for tree Table, second param is expanded) | row, (expandedRows \| expanded) |
### Méthodes de Table
@ -1874,7 +1878,7 @@ Vous pouvez personnaliser les indices des colonnes de type `index`.
| clearSelection | Dans les tables avec sélection multiple, efface la sélection. | — |
| toggleRowSelection | Dans les tables avec sélection multiple, change la sélection d'une ligne. Grâce au deuxième paramètre vous pouvez directement décider si cette ligne est sélectionnée. | row, selected |
| toggleAllSelection | Utilisé dans les tables à sélection multiples, sélectionne toutes les lignes. | - |
| toggleRowExpansion | Pour les lignes extensibles, change l'état de la ligne. Grâce au deuxième paramètre vous pouvez directement décider si cette ligne est étendue. | row, expanded |
| toggleRowExpansion | used in expandable Table or tree Table, toggle if a certain row is expanded. With the second parameter, you can directly set if this row is expanded or collapsed | row, expanded |
| setCurrentRow | Dans les tables à sélection simple, sélectionne une ligne. Sans paramètre la sélection est effacé. | row |
| clearSort | Efface le tri. | — |
| clearFilter | Efface les filtres des colonnes dont les `columnKey` sont passées. Si aucun paramètre, efface tout les filtres. | columnKeys |

View File

@ -1277,7 +1277,7 @@
### 树形数据与懒加载
:::demo 支持树类型的数据。此时,必须要指定 `row-key`。支持子节点数据异步加载。设置 Table 的 `lazy` 属性为 true 与 加载函数 `load` ,指定 row 中的 `hasChildren` 来确定哪些行是包含子节点
:::demo 支持树类型的数据的显示。当 row 中包含 `children` 字段时,被视为树形数据。渲染树形数据时,必须要指定 `row-key`。支持子节点数据异步加载。设置 Table 的 `lazy` 属性为 true 与加载函数 `load` 。通过指定 row 中的 `hasChildren` 字段来指定哪些行是包含子节点。`children` 与 `hasChildren` 都可以通过 `tree-props` 配置
```html
<template>
@ -1285,8 +1285,10 @@
<el-table
:data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
row-key="id">
default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="date"
label="日期"
@ -1312,7 +1314,7 @@
border
lazy
:load="load"
>
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="date"
label="日期"
@ -1392,19 +1394,21 @@
},
methods: {
load(tree, treeNode, resolve) {
resolve([
{
id: 31,
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}, {
id: 32,
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}
])
setTimeout(() => {
resolve([
{
id: 31,
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}, {
id: 32,
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}
])
}, 1000)
}
},
}
@ -1867,7 +1871,7 @@
| header-cell-style | 表头单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有表头单元格设置一样的 Style。 | Function({row, column, rowIndex, columnIndex})/Object | — | — |
| row-key | 行数据的 Key用来优化 Table 的渲染;在使用 reserve-selection 功能与显示树形数据时,该属性是必填的。类型为 String 时,支持多层访问:`user.info.id`,但不支持 `user.info[0].id`,此种情况请使用 `Function`。 | Function(row)/String | — | — |
| empty-text | 空数据时显示的文本内容,也可以通过 `slot="empty"` 设置 | String | — | 暂无数据 |
| default-expand-all | 是否默认展开所有行,当 Table 中存在 type="expand" 的 Column 的时候有效 | Boolean | — | false |
| default-expand-all | 是否默认展开所有行,当 Tale 包含展开行存在或者为树形表格时有效 | Boolean | — | false |
| expand-row-keys | 可以通过该属性设置 Table 目前的展开行,需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。| Array | — | |
| default-sort | 默认的排序列的 prop 和顺序。它的`prop`属性指定默认的排序的列,`order`指定默认排序的顺序| Object | `order`: ascending, descending | 如果只指定了`prop`, 没有指定`order`, 则默认顺序是ascending |
| tooltip-effect | tooltip `effect` 属性 | String | dark/light | | dark |
@ -1878,7 +1882,8 @@
| select-on-indeterminate | 在多选表格中,当仅有部分行被选中时,点击表头的多选框时的行为。若为 true则选中所有行若为 false则取消选择所有行 | Boolean | — | true |
| indent | 展示树形数据时,树节点的缩进 | Number | — | 16 |
| lazy | 是否懒加载子节点数据 | Boolean | — | — |
| load | 加载子节点数据的函数lazy 为 true 时生效 | Function({ row, treeNode, resolve }) | — | — |
| load | 加载子节点数据的函数lazy 为 true 时生效,函数第二个参数包含了节点的层级信息 | Function(row, treeNode, resolve) | — | — |
| tree-props | 渲染嵌套数据的配置选项 | Object | — | { hasChildren: 'hasChildren', children: 'children' } |
### Table Events
| 事件名 | 说明 | 参数 |
@ -1899,7 +1904,7 @@
| filter-change | 当表格的筛选条件发生变化的时候会触发该事件,参数的值是一个对象,对象的 key 是 column 的 columnKey对应的 value 为用户选择的筛选条件的数组。 | filters |
| current-change | 当表格的当前行发生变化的时候会触发该事件,如果要高亮当前行,请打开表格的 highlight-current-row 属性 | currentRow, oldCurrentRow |
| header-dragend | 当拖动表头改变了列的宽度的时候会触发该事件 | newWidth, oldWidth, column, event |
| expand-change | 当用户对某一行展开或者关闭的时候会触发该事件 | row, expandedRows |
| expand-change | 当用户对某一行展开或者关闭的时候会触发该事件(展开行时,回调的第二个参数为 expandedRows树形表格时第二参数为 expanded | row, (expandedRows \| expanded) |
### Table Methods
| 方法名 | 说明 | 参数 |
@ -1907,7 +1912,7 @@
| clearSelection | 用于多选表格,清空用户的选择 | — |
| toggleRowSelection | 用于多选表格切换某一行的选中状态如果使用了第二个参数则是设置这一行选中与否selected 为 true 则选中) | row, selected |
| toggleAllSelection | 用于多选表格,切换所有行的选中状态 | - |
| toggleRowExpansion | 用于可展开表格切换某一行的展开状态如果使用了第二个参数则是设置这一行展开与否expanded 为 true 则展开) | row, expanded |
| toggleRowExpansion | 用于可展开表格与树形表格切换某一行的展开状态如果使用了第二个参数则是设置这一行展开与否expanded 为 true 则展开) | row, expanded |
| setCurrentRow | 用于单选表格,设定某一行为选中行,如果调用时不加参数,则会取消目前高亮行的选中状态。 | row |
| clearSort | 用于清空排序条件,数据会恢复成未排序的状态 | — |
| clearFilter | 不传入参数时用于清空所有过滤条件数据会恢复成未过滤的状态也可传入由columnKey组成的数组以清除指定列的过滤条件 | columnKey |

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

@ -72,17 +72,6 @@
}
},
customRender(h) {
return (<div class="el-table-filter">
<div class="el-table-filter__content">
</div>
<div class="el-table-filter__bottom">
<button on-click={ this.handleConfirm }>{ this.t('el.table.confirmFilter') }</button>
<button on-click={ this.handleReset }>{ this.t('el.table.resetFilter') }</button>
</div>
</div>);
},
methods: {
isActive(filter) {
return filter.value === this.filterValue;

View File

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

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,37 @@
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];
});
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,155 @@
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.updateCurrentRow();
this.updateExpandRows();
if (!states.reserveSelection) {
if (dataInstanceChanged) {
this.clearSelection();
} else {
this.cleanSelection();
}
} else {
this.assertRowKey();
this.updateSelectionByRowKey();
}
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 } = options;
if (prop) {
// TODOnextTick 是否有必要?
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) {
// 修复 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) {
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) {
const oldCurrentRow = states.currentRow;
states.currentRow = row;
if (oldCurrentRow !== row) {
this.table.$emit('current-change', row, oldCurrentRow);
}
}
};
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,193 @@
import { walkTreeNode, getRowIdentity } from '../util';
export default {
data() {
return {
states: {
// defaultExpandAll 存在于 expand.js 中,这里不重复添加
// TODO: 拆分为独立的 TreeTale在 expand 中,展开行的记录是放在 expandRows 中,统一用法
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',
// expandRowKeys 在 TreeTable 中也有使用
expandRowKeys: '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);
if (!keys.length) return;
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 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] = {
loaded: !!loaded,
loading: !!loading,
expanded: getExpanded(oldValue, key),
children: lazyNodeChildren,
level: ''
};
}
});
}
this.states.treeData = newTreeData;
this.updateTableScrollY();
},
updateTreeExpandKeys(value) {
// 仅仅在包含嵌套数据时才去更新
if (Object.keys(this.normalizedData).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, 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,388 @@
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 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,
currentRow: 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 (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
});
},
// 适配层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

@ -1,9 +1,11 @@
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',
@ -28,30 +30,7 @@ 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 || [];
return (
<table
class="el-table__body"
@ -60,93 +39,16 @@ export default {
border="0">
<colgroup>
{
this._l(this.columns, column => <col name={ column.id } />)
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
</colgroup>
<tbody>
{
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 = (<tr
v-show={ treeNode ? treeNode.display : true }
style={ this.rowStyle ? this.getRowStyle(row, $index) : null }
key={ rowKey }
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() }
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 (
<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,
h,
data,
columnsHidden[cellIndex]
)
}
</td>
);
}
})
}
</tr>);
if (this.hasExpandColumn && this.store.isRowExpanded(row)) {
return [
tr,
<tr>
<td colspan={ this.columns.length } class="el-table__expanded-cell">
{ this.table.renderExpanded ? this.table.renderExpanded(h, { row, $index, store: this.store }) : ''}
</td>
</tr>
];
} else {
return tr;
}
}).concat(
<el-tooltip effect={ this.table.tooltipEffect } placement="top" ref="tooltip" content={ this.tooltipContent }></el-tooltip>
)
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>
);
@ -157,53 +59,20 @@ 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',
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;
return arrayFindIndex(this.columns, ({ type }) => type === 'default');
}
},
@ -211,16 +80,22 @@ 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) {
if (!this.store.states.isComplex) return;
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');
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');
}
});
}
},
@ -256,7 +131,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({
@ -265,7 +139,6 @@ export default {
rowIndex,
columnIndex
});
if (Array.isArray(result)) {
rowspan = result[0];
colspan = result[1];
@ -274,11 +147,7 @@ export default {
colspan = result.colspan;
}
}
return {
rowspan,
colspan
};
return { rowspan, colspan };
},
getRowStyle(row, rowIndex) {
@ -289,7 +158,7 @@ export default {
rowIndex
});
}
return rowStyle;
return rowStyle || null;
},
getRowClass(row, rowIndex) {
@ -353,6 +222,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);
@ -401,13 +278,13 @@ export default {
this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
},
handleMouseEnter(index) {
handleMouseEnter: debounce(30, function(index) {
this.store.commit('setHoverRow', index);
},
}),
handleMouseLeave() {
handleMouseLeave: debounce(30, function() {
this.store.commit('setHoverRow', null);
},
}),
handleContextMenu(event, row) {
this.handleEvent(event, row, 'contextmenu');
@ -435,9 +312,169 @@ export default {
table.$emit(`row-${name}`, row, column, event);
},
handleExpandClick(row, e) {
e.stopPropagation();
this.store.toggleRowExpansion(row);
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 (<tr
v-show={display}
style={ 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
};
// TODO: 优化这里的逻辑
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.loaded === 'boolean' && cur.loaded) {
treeRowData.noLazyChildren = !(cur.children && cur.children.length);
}
if (typeof cur.loading === 'boolean') {
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.loaded === 'boolean' && cur.loaded) {
innerTreeRowData.noLazyChildren = !(cur.children && cur.children.length);
}
if (typeof cur.lazy === 'boolean') {
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

@ -1,141 +1,9 @@
import { cellStarts, cellForced, defaultRenderCell, treeCellPrefix } 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 <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 }, proxy) {
const expanded = store.states.expandRows.indexOf(row) > -1;
return <div class={ 'el-table__expand-icon ' + (expanded ? 'el-table__expand-icon--expanded' : '') }
on-click={ e => proxy.handleExpandClick(row, e) }>
<i class='el-icon el-icon-arrow-right'></i>
</div>;
},
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,125 +74,237 @@ export default {
}
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;
}
},
created() {
this.customRender = this.$options.render;
this.$options.render = h => h('div', this.$slots.default);
methods: {
getPropsData(...props) {
return props.reduce((prev, cur) => {
if (Array.isArray(cur)) {
cur.forEach((key) => {
prev[key] = this[key];
});
}
return prev;
}, {});
},
let parent = this.columnOrTableParent;
let owner = this.owner;
this.isSubColumn = owner !== parent;
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) {
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;
};
}
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'];
// 一些属性具有别名
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++;
let type = this.type;
const width = parseWidth(this.width);
const minWidth = parseMinWidth(this.minWidth);
let isColumnGroup = false;
let column = getDefaultColumn(type, {
const type = this.type || 'default';
const sortable = this.sortable === '' ? true : this.sortable;
const defaults = {
...cellStarts[type],
id: this.columnId,
columnKey: this.columnKey,
label: this.label,
className: this.className,
labelClassName: this.labelClassName,
type: type,
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,
align: this.realAlign,
headerAlign: this.realHeaderAlign,
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,
// filter 相关属性
filterable: this.filters || this.filterMethod,
filterMultiple: this.filterMultiple,
filteredValue: [],
filterPlacement: '',
isColumnGroup: false,
filterOpened: false,
filteredValue: this.filteredValue || [],
filterPlacement: this.filterPlacement || '',
index: this.index,
sortOrders: this.sortOrders
});
// sort 相关属性
sortable: sortable,
// index 列
index: this.index
};
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;
}
});
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'];
// 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.');
}
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;
let renderCell = column.renderCell;
let _self = this;
// 注册 watcher
this.registerNormalWatchers();
this.registerComplexWatchers();
},
if (type === 'expand') {
owner.renderExpanded = function(h, data) {
return _self.$scopedSlots.default
? _self.$scopedSlots.default(data)
: _self.$slots.default;
};
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);
column.renderCell = function(h, data) {
return <div class="cell">{ renderCell(h, data, this._renderProxy) }</div>;
};
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
? <div class="cell el-tooltip" style={ {width: (data.column.realWidth || data.column.width) - 1 + 'px'} }>{ children }</div>
: (<div class="cell">
{ children }
</div>);
};
owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
},
destroyed() {
@ -345,150 +313,8 @@ export default {
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;
}
},
filters(newVal) {
if (this.columnConfig) {
this.columnConfig.filters = newVal;
}
},
filterMultiple(newVal) {
if (this.columnConfig) {
this.columnConfig.filterMultiple = newVal;
}
},
align(newVal) {
if (this.columnConfig) {
this.columnConfig.align = newVal ? 'is-' + newVal : null;
if (!this.headerAlign) {
this.columnConfig.headerAlign = newVal ? 'is-' + newVal : null;
}
}
},
headerAlign(newVal) {
if (this.columnConfig) {
this.columnConfig.headerAlign = 'is-' + (newVal ? newVal : this.align);
}
},
width(newVal) {
if (this.columnConfig) {
this.columnConfig.width = parseWidth(newVal);
this.owner.store.scheduleLayout();
}
},
minWidth(newVal) {
if (this.columnConfig) {
this.columnConfig.minWidth = parseMinWidth(newVal);
this.owner.store.scheduleLayout();
}
},
fixed(newVal) {
if (this.columnConfig) {
this.columnConfig.fixed = newVal;
this.owner.store.scheduleLayout(true);
}
},
sortable(newVal) {
if (this.columnConfig) {
this.columnConfig.sortable = newVal;
}
},
index(newVal) {
if (this.columnConfig) {
this.columnConfig.index = newVal;
}
},
formatter(newVal) {
if (this.columnConfig) {
this.columnConfig.formatter = newVal;
}
},
className(newVal) {
if (this.columnConfig) {
this.columnConfig.className = newVal;
}
},
labelClassName(newVal) {
if (this.columnConfig) {
this.columnConfig.labelClassName = newVal;
}
}
},
methods: {
renderTreeCell(data) {
if (!data.treeNode) return null;
const ele = [];
ele.push(<span class="el-table__indent" style={{'padding-left': data.treeNode.indent + 'px'}}></span>);
if (data.treeNode.hasChildren) {
ele.push(<div class={ ['el-table__expand-icon', data.treeNode.expanded ? 'el-table__expand-icon--expanded' : '']}
on-click={this.handleTreeExpandIconClick.bind(this, data)}>
<i class='el-icon el-icon-arrow-right'></i>
</div>);
} else {
ele.push(<span class="el-table__placeholder"></span>);
}
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);
}
}
},
mounted() {
const owner = this.owner;
const parent = this.columnOrTableParent;
let columnIndex;
if (!this.isSubColumn) {
columnIndex = [].indexOf.call(parent.$refs.hiddenColumns.children, this.$el);
} else {
columnIndex = [].indexOf.call(parent.$el.children, this.$el);
}
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);
}
}
owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
render(h) {
// slots 也要渲染,需要计算合并表头
return h('div', this.$slots.default);
}
};

View File

@ -1,4 +1,5 @@
import LayoutObserver from './layout-observer';
import { mapStates } from './store/helper';
export default {
name: 'ElTableFooter',
@ -49,7 +50,7 @@ export default {
border="0">
<colgroup>
{
this._l(this.columns, column => <col name={ column.id } />)
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
{
this.hasGutter ? <col name="gutter" /> : ''
@ -58,18 +59,17 @@ export default {
<tbody class={ [{ 'has-gutter': this.hasGutter }] }>
<tr>
{
this._l(this.columns, (column, cellIndex) =>
<td
colspan={ column.colSpan }
rowspan={ column.rowSpan }
class={ this.getRowClasses(column, cellIndex) }>
<div class={ ['cell', column.labelClassName] }>
{
sums[cellIndex]
}
</div>
</td>
)
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> : ''
@ -104,37 +104,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: {

View File

@ -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 = [];
@ -82,7 +82,7 @@ export default {
border="0">
<colgroup>
{
this._l(this.columns, column => <col name={ column.id } />)
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
{
this.hasGutter ? <col name="gutter" /> : ''
@ -96,42 +96,44 @@ export default {
class={ this.getHeaderRowClass(rowIndex) }
>
{
this._l(columns, (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>
)
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> : ''
@ -162,8 +164,7 @@ export default {
},
components: {
ElCheckbox,
ElTag
ElCheckbox
},
computed: {
@ -171,37 +172,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() {

View File

@ -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,49 +53,19 @@ 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');
}
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;
const headerHeight = this.headerHeight = !this.showHeader ? 0 : headerWrapper.offsetHeight;
if (this.showHeader && headerWrapper.offsetWidth > 0 && (this.table.columns || []).length > 0 && headerHeight < 2) {
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;
this.bodyHeight = tableHeight - headerHeight - footerHeight + (footerWrapper ? 1 : 0);
}
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;
this.updateScrollY();
this.notifyObservers('scrollable');
this.setHeight(value, 'max-height');
}
getFlattenColumns() {
@ -111,6 +82,30 @@ class TableLayout {
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;
const headerHeight = this.headerHeight = !this.showHeader ? 0 : headerWrapper.offsetHeight;
if (this.showHeader && 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.table.data || this.table.data.length === 0;
this.viewportHeight = this.scrollX ? tableHeight - (noData ? 0 : this.gutterWidth) : tableHeight;
this.updateScrollY();
this.notifyObservers('scrollable');
}
updateColumnsWidth() {
if (Vue.prototype.$isServer) return;
const fit = this.fit;

View File

@ -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;

View File

@ -214,36 +214,17 @@
<script type="text/babel">
import ElCheckbox from 'element-ui/packages/checkbox';
import debounce from 'throttle-debounce/debounce';
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 TableStore from './table-store';
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 { 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;
}
};
import { parseHeight } from './util';
let tableIdSeed = 1;
@ -338,6 +319,16 @@
default: 16
},
treeProps: {
type: Object,
default() {
return {
hasChildren: 'hasChildren',
children: 'children'
};
}
},
lazy: Boolean,
load: Function
@ -369,7 +360,7 @@
},
toggleRowExpansion(row, expanded) {
this.store.toggleRowExpansion(row, expanded);
this.store.toggleRowExpansionAdapter(row, expanded);
},
clearSelection() {
@ -413,37 +404,42 @@
handleHeaderFooterMousewheel(event, data) {
const { pixelX, pixelY } = data;
if (Math.abs(pixelX) >= Math.abs(pixelY)) {
event.preventDefault();
this.bodyWrapper.scrollLeft += data.pixelX / 5;
}
},
// TODO
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() {
const { headerWrapper, footerWrapper } = this.$refs;
const refs = this.$refs;
let self = this;
this.bodyWrapper.addEventListener('scroll', function() {
if (headerWrapper) headerWrapper.scrollLeft = this.scrollLeft;
if (footerWrapper) footerWrapper.scrollLeft = this.scrollLeft;
if (refs.fixedBodyWrapper) refs.fixedBodyWrapper.scrollTop = this.scrollTop;
if (refs.rightFixedBodyWrapper) refs.rightFixedBodyWrapper.scrollTop = this.scrollTop;
const maxScrollLeftPosition = this.scrollWidth - this.offsetWidth - 1;
const scrollLeft = this.scrollLeft;
if (scrollLeft >= maxScrollLeftPosition) {
self.scrollPosition = 'right';
} else if (scrollLeft === 0) {
self.scrollPosition = 'left';
} else {
self.scrollPosition = 'middle';
}
});
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;
@ -480,61 +476,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: {
@ -553,40 +496,24 @@
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' : '';
},
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 {};
},
@ -597,19 +524,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 {};
},
@ -633,7 +559,15 @@
height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
};
}
}
},
...mapStates({
selection: 'selection',
columns: 'columns',
tableData: 'data',
fixedColumns: 'fixedColumns',
rightFixedColumns: 'rightFixedColumns'
})
},
watch: {
@ -658,8 +592,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(() => {
@ -673,14 +605,15 @@
immediate: true,
handler(newVal) {
if (newVal) {
this.store.setExpandRowKeys(newVal);
this.store.setExpandRowKeysAdapter(newVal);
}
}
}
},
destroyed() {
if (this.resizeListener) removeResizeListener(this.$el, this.resizeListener);
created() {
this.tableId = 'el-table_' + tableIdSeed++;
this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
},
mounted() {
@ -707,23 +640,30 @@
this.$ready = true;
},
destroyed() {
this.unbindEvents();
},
data() {
const store = new TableStore(this, {
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,
lazyColumnIdentifier: hasChildren,
childrenColumnName: children
});
const layout = new TableLayout({
store,
store: this.store,
table: this,
fit: this.fit,
showHeader: this.showHeader
});
return {
layout,
store,
isHidden: false,
renderExpanded: null,
resizeProxyVisible: false,

View File

@ -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;
};
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) {
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);
}
});
}

View File

@ -561,9 +561,11 @@
[class*=el-table__row--level] {
.el-table__expand-icon {
display: inline-block;
width: 14px;
vertical-align: middle;
margin-right: 5px;
width: 20px;
line-height: 20px;
height: 20px;
text-align: center;
margin-right: 3px;
}
}
}

View File

@ -1,4 +1,4 @@
import { createVue, triggerEvent, destroyVM, waitImmediate } from '../util';
import { createVue, triggerEvent, destroyVM, waitImmediate, wait } from '../util';
const DELAY = 10;
const testDataArr = [];
@ -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);
@ -1760,7 +1760,7 @@ describe('Table', () => {
});
});
it('hover', done => {
it('hover', async() => {
const vm = createVue({
template: `
<el-table :data="testData">
@ -1776,19 +1776,17 @@ describe('Table', () => {
};
}
}, true);
setTimeout(_ => {
const tr = vm.$el.querySelector('.el-table__body-wrapper tbody tr');
triggerEvent(tr, 'mouseenter', true, false);
setTimeout(_ => {
expect(tr.classList.contains('hover-row')).to.true;
triggerEvent(tr, 'mouseleave', true, false);
setTimeout(_ => {
expect(tr.classList.contains('hover-row')).to.false;
destroyVM(vm);
done();
}, DELAY);
}, DELAY);
}, DELAY);
await waitImmediate();
const tr = vm.$el.querySelector('.el-table__body-wrapper tbody tr');
triggerEvent(tr, 'mouseenter', true, false);
await wait(50);
expect(tr.classList.contains('hover-row')).to.true;
triggerEvent(tr, 'mouseleave', true, false);
await wait(50);
expect(tr.classList.contains('hover-row')).to.false;
destroyVM(vm);
});
it('highlight-current-row', done => {
@ -1910,96 +1908,204 @@ describe('Table', () => {
}, DELAY);
});
it('render tree structual data', (done) => {
const vm = createVue({
template: `
<el-table :data="testData" row-key="release">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[1].children = [
{
name: 'A Bug\'s Life copy 1', release: '1998-11-25-1', director: 'John Lasseter', runtime: 95
},
{
name: 'A Bug\'s Life copy 2', release: '1998-11-25-2', director: 'John Lasseter', runtime: 95
}
];
return {
testData: testData
};
}
}, true);
setTimeout(() => {
const rows = vm.$el.querySelectorAll('.el-table__row');
expect(rows.length).to.equal(7);
const childRows = vm.$el.querySelectorAll('.el-table__row--level-1');
expect(childRows.length).to.equal(2);
childRows.forEach(item => {
expect(item.style.display).to.equal('none');
});
vm.$el.querySelector('.el-table__expand-icon').click();
setTimeout(() => {
childRows.forEach(item => {
expect(item.style.display).to.equal('');
});
done();
}, DELAY);
}, DELAY);
});
it('load substree row data', async() => {
const vm = createVue({
template: `
<el-table :data="testData" row-key="release" lazy :load="load">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[testData.length - 1].children = [
{
name: 'A Bug\'s Life copy 1', release: '2008-1-25-1', director: 'John Lasseter', runtime: 95
}
];
testData[1].hasChildren = true;
return {
testData: testData
};
},
methods: {
load(row, treeNode, resolve) {
resolve([
describe('tree', () => {
let vm;
afterEach(() => destroyVM(vm));
it('render tree structual data', async() => {
vm = createVue({
template: `
<el-table :data="testData" row-key="release">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[1].children = [
{
name: 'A Bug\'s Life copy 1', release: '1998-11-25-1', director: 'John Lasseter', runtime: 95
},
{
name: 'A Bug\'s Life copy 2', release: '1998-11-25-2', director: 'John Lasseter', runtime: 95
}
]);
];
return {
testData: testData
};
}
}
}, true);
}, true);
await waitImmediate();
await waitImmediate();
const rows = vm.$el.querySelectorAll('.el-table__row');
expect(rows.length).to.equal(7);
const childRows = vm.$el.querySelectorAll('.el-table__row--level-1');
expect(childRows.length).to.equal(2);
childRows.forEach(item => {
expect(item.style.display).to.equal('none');
});
vm.$el.querySelector('.el-table__expand-icon').click();
const expandIcon = vm.$el.querySelector('.el-table__expand-icon');
expandIcon.click();
await waitImmediate();
childRows.forEach(item => {
expect(item.style.display).to.equal('');
});
});
await waitImmediate();
it('load substree row data', async() => {
vm = createVue({
template: `
<el-table :data="testData" row-key="release" lazy :load="load">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[testData.length - 1].children = [
{
name: 'A Bug\'s Life copy 1', release: '2008-1-25-1', director: 'John Lasseter', runtime: 95
}
];
testData[1].hasChildren = true;
return {
testData: testData
};
},
methods: {
load(row, treeNode, resolve) {
resolve([
{
name: 'A Bug\'s Life copy 1', release: '1998-11-25-1', director: 'John Lasseter', runtime: 95
},
{
name: 'A Bug\'s Life copy 2', release: '1998-11-25-2', director: 'John Lasseter', runtime: 95
}
]);
}
}
}, true);
await waitImmediate();
expect(expandIcon.classList.contains('el-table__expand-icon--expanded')).to.be.true;
expect(vm.$el.querySelectorAll('.el-table__row').length).to.equal(8);
const expandIcon = vm.$el.querySelector('.el-table__expand-icon');
expandIcon.click();
await waitImmediate();
expect(expandIcon.classList.contains('el-table__expand-icon--expanded')).to.be.true;
expect(vm.$el.querySelectorAll('.el-table__row').length).to.equal(8);
});
it('tree-props & default-expand-all & expand-change', async() => {
const spy = sinon.spy();
vm = createVue({
template: `
<el-table
:data="testData" lazy default-expand-all row-key="release" :tree-props="{children: 'childrenTest', hasChildren: 'hasChildrenTest'}"
:load="load" @expand-change="change">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[testData.length - 1].childrenTest = [
{
name: 'A Bug\'s Life copy 1', release: '2008-1-25-1', director: 'John Lasseter', runtime: 95
}
];
testData[1].hasChildrenTest = true;
return {
testData: testData
};
},
methods: {
load(row, treeNode, resolve) {
resolve([
{
name: 'A Bug\'s Life copy 1', release: '1998-11-25-1', director: 'John Lasseter', runtime: 95
},
{
name: 'A Bug\'s Life copy 2', release: '1998-11-25-2', director: 'John Lasseter', runtime: 95
}
]);
},
change: spy
}
}, true);
await waitImmediate();
const childRows = vm.$el.querySelectorAll('.el-table__row--level-1');
childRows.forEach(item => {
expect(item.style.display).to.equal('');
});
const expandIcon = vm.$el.querySelector('.el-table__expand-icon');
expandIcon.click();
await waitImmediate();
expect(expandIcon.classList.contains('el-table__expand-icon--expanded')).to.be.true;
expect(vm.$el.querySelectorAll('.el-table__row').length).to.equal(8);
expect(spy.args[0][0]).to.be.an('object');
expect(spy.args[0][1]).to.be.true;
});
it('expand-row-keys & toggleRowExpansion', async() => {
vm = createVue({
template: `
<el-table :data="testData" row-key="release" lazy :load="load" :expand-row-keys="['2003-5-30']" ref="table">
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
<el-table-column prop="runtime" label="时长(分)" />
</el-table>
`,
data() {
const testData = getTestData();
testData[testData.length - 1].children = [
{
name: 'A Bug\'s Life copy 1', release: '2003-5-30-1', director: 'John Lasseter', runtime: 95,
hasChildren: true
}
];
return {
testData: testData
};
},
methods: {
load(row, treeNode, resolve) {
resolve([
{
name: 'A Bug\'s Life copy 1', release: '2003-5-30-2', director: 'John Lasseter', runtime: 95
}
]);
},
closeExpandRow() {
const testData = this.testData;
const row = testData[testData.length - 1].children[0];
this.$refs.table.toggleRowExpansion(row);
}
}
}, true);
await waitImmediate();
const childRows = vm.$el.querySelectorAll('.el-table__row--level-1');
childRows.forEach(item => {
expect(item.style.display).to.equal('');
});
const expandIcon = childRows[0].querySelector('.el-table__expand-icon');
expandIcon.click();
await waitImmediate();
expect(expandIcon.classList.contains('el-table__expand-icon--expanded')).to.be.true;
vm.closeExpandRow();
await waitImmediate();
expect(expandIcon.classList.contains('el-table__expand-icon--expanded')).to.be.false;
});
});
});