Переглянути джерело

Update Table:
1. Add rowKey prop for Table.
2. Add clearSelection method for Table.
2. Add reserveSelection prop for TableColumn[type="selection"]

furybean 8 роки тому
батько
коміт
6ab0d57428

+ 2 - 0
CHANGELOG.md

@@ -25,6 +25,8 @@
 
 - 全屏 Loading 现在默认不再锁定屏幕滚动。如果需要的话,可添加 `lock` 修饰符
 - Table 删除属性 fixedColumnCount, customCriteria, customBackgroundColors
+- Table 的 allow-no-selection 属性更名为 allow-no-current-row
+- Table 的 selectionchange、cellmouseenter、cellmouseleave、cellclick 事件更名为 selection-change、cell-mouseenter、cell-mouseleave、cell-click。
 
 ### 1.0.0-rc.7
 

+ 22 - 12
examples/docs/zh-cn/table.md

@@ -670,15 +670,15 @@
 
 选择单行数据时使用色块表示。
 
-:::demo Table 组件提供了选择的支持,只需要配置`selection-mode`属性即可实现单选(`single`)、多选(`multiple`),如果不需要则设置为`none`。之后由`selectionchange`事件来管理选中时触发的事件,它会传入一个`value`,`value`为生成表格时的对应对象。本例中还使用了`allow-no-selection`属性,它接受一个`Boolean`,若为`true`,则允许为空,默认为`false`,此时将会产生默认值,为填入数组的第一个对象。如果需要显示索引,可以增加一列`el-table-column`,设置`type`属性为`index`即可显示从 1 开始的索引号。
+:::demo Table 组件提供了选择的支持,只需要配置`selection-mode`属性即可实现单选(`single`)、多选(`multiple`),如果不需要则设置为`none`。之后由`selection-change`事件来管理选中时触发的事件,它会传入一个`value`,`value`为生成表格时的对应对象。本例中还使用了`allow-no-current-row`属性,它接受一个`Boolean`,若为`true`,则允许为空,默认为`false`,此时将会产生默认值,为填入数组的第一个对象。如果需要显示索引,可以增加一列`el-table-column`,设置`type`属性为`index`即可显示从 1 开始的索引号。
 ```html
 <template>
   <el-table
     :data="tableData"
     selection-mode="single"
-    @selectionchange="handleSelectionChange"
+    @selection-change="handleSelectionChange"
     style="width: 100%"
-    allow-no-selection>
+    allow-no-current-row>
     <el-table-column
       type="index"
       width="50">
@@ -739,14 +739,14 @@
 
 选择多行数据时使用 Checkbox。
 
-:::demo 除了`selection-mode`的设置外,多选与单选并没有太大差别,只是传入`selectionchange`事件中的参数从对象变成了对象数组。此外,需要提供一个列来显示多选框: 手动添加一个`el-table-column`,设`type`属性为`selection`即可。在本例中,为了方便说明其他属性,我们还使用了`inline-template`和`show-tooltip-when-overflow`:设置了`inline-template`属性后,可以通过调用`row`对象中的值取代`prop`属性的设置;默认情况下若内容过多会折行显示,若需要单行显示可以使用`show-tooltip-when-overflow`属性,它接受一个`Boolean`,为`true`时多余的内容会在 hover 时以 tooltip 的形式显示出来。
+:::demo 除了`selection-mode`的设置外,多选与单选并没有太大差别,只是传入`selection-change`事件中的参数从对象变成了对象数组。此外,需要提供一个列来显示多选框: 手动添加一个`el-table-column`,设`type`属性为`selection`即可。在本例中,为了方便说明其他属性,我们还使用了`inline-template`和`show-tooltip-when-overflow`:设置了`inline-template`属性后,可以通过调用`row`对象中的值取代`prop`属性的设置;默认情况下若内容过多会折行显示,若需要单行显示可以使用`show-tooltip-when-overflow`属性,它接受一个`Boolean`,为`true`时多余的内容会在 hover 时以 tooltip 的形式显示出来。
 ```html
 <template>
   <el-table
     :data="tableData3"
     selection-mode="multiple"
     style="width: 100%"
-    @selectionchange="handleMultipleSelectionChange">
+    @selection-change="handleMultipleSelectionChange">
     <el-table-column
       type="selection"
       width="50">
@@ -888,17 +888,26 @@
 | stripe | 是否为斑马纹 table | boolean | — | false |
 | border | 是否带有纵向边框 | boolean | — | false |
 | fit | 列的宽度是否自撑开 | boolean | — | true |
-| rowClassName | 行的 className 的回调,会传入 row, index。 | Function | - | - |
+| row-class-name | 行的 className 的回调,会传入 row, index。 | Function | - | - |
+| row-key | 行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能的情况下,该属性是必填的 | Function, String | - | |
 | selection-mode | 列表项选择模式 | string | single/multiple/none | none |
-| allow-no-selection | 单选模式是否允许选项为空 | boolean | — | false |
+| allow-no-current-row | 单选模式是否允许选项为空 | boolean | — | false |
 
 ### Table Events
 | 事件名 | 说明 | 参数 |
 | ---- | ---- | ---- |
-| selectionchange | 当选择项发生变化时会触发该事件 | selected |
-| cellmouseenter | 当单元格 hover 进入时会触发该事件 | row, column, cell, event |
-| cellmouseleave | 当单元格 hover 退出时会触发该事件 | row, column, cell, event |
-| cellclick | 当某个单元格被点击时会触发该事件 | row, column, cell, event |
+| select | 当用户手动勾选数据行的 Checkbox 时触发的事件 | selection |
+| select-all | 当用户手动勾选全选 Checkbox 时触发的事件 | selection |
+| selection-change | 当选择项发生变化时会触发该事件 | selection |
+| cell-mouseenter | 当单元格 hover 进入时会触发该事件 | row, column, cell, event |
+| cell-mouseleave | 当单元格 hover 退出时会触发该事件 | row, column, cell, event |
+| cell-click | 当某个单元格被点击时会触发该事件 | row, column, cell, event |
+| row-click | 当某一行被点击时会触发该事件 | row, event |
+
+### Table Methods
+| 方法名 | 说明 | 参数 |
+| ---- | ---- | ---- |
+| clearSelection | 清空用户的选择,当使用 reserve-selection 功能的时候,可能会需要使用此方法 | selection |
 
 ### Table-column Attributes
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
@@ -914,4 +923,5 @@
 | show-tooltip-when-overflow | 当过长被隐藏时显示 tooltip | Boolean | — | false |
 | inline-template | 指定该属性后可以自定义 column 模板,参考多选的时间列,通过 row 获取行信息,JSX 里通过 _self 获取当前上下文。此时不需要配置 prop 属性  | — | — |
 | align | 对齐方式 | String | left, center, right | left |
-| selectable | 仅对 type=selection 的列有效,类型为 Function,Function 的返回值用来决定这一行的 CheckBox 是否可以勾选 | Function | - |
+| selectable | 仅对 type=selection 的列有效,类型为 Function,Function 的返回值用来决定这一行的 CheckBox 是否可以勾选 | Function | - | - |
+| reserve-selection | 仅对 type=selection 的列有效,类型为 Boolean,为 true 则代表会保留之前数据的选项,需要配合 Table 的 clearSelection 方法使用。 | Boolean | - | false |

+ 6 - 8
packages/table/src/table-body.js

@@ -134,7 +134,7 @@ export default {
       if (cell) {
         const column = getColumnByCell(table, cell);
         const hoverState = table.hoverState = { cell, column, row };
-        table.$emit('cellmouseenter', hoverState.row, hoverState.column, hoverState.cell, event);
+        table.$emit('cell-mouseenter', hoverState.row, hoverState.column, hoverState.cell, event);
       }
 
       // 判断是否text-overflow, 如果是就显示tooltip
@@ -145,12 +145,10 @@ export default {
 
     handleCellMouseLeave(event) {
       const cell = getCell(event);
+      if (!cell) return;
 
-      if (cell) {
-        const table = this.$parent;
-        const oldHoverState = table.hoverState;
-        table.$emit('cellmouseleave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
-      }
+      const oldHoverState = this.$parent.hoverState;
+      this.$parent.$emit('cell-mouseleave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
     },
 
     handleMouseEnter(index) {
@@ -164,13 +162,13 @@ export default {
       if (cell) {
         const column = getColumnByCell(table, cell);
         if (column) {
-          table.$emit('cellclick', row, column, cell, event);
+          table.$emit('cell-click', row, column, cell, event);
         }
       }
 
       this.store.commit('setSelectedRow', row);
 
-      table.$emit('rowclick', row, event);
+      table.$emit('row-click', row, event);
     },
 
     getCellContent(row, property, columnId) {

+ 4 - 2
packages/table/src/table-column.js

@@ -49,7 +49,7 @@ const forced = {
     headerTemplate: function(h, label) {
       return <div>{ label || '#' }</div>;
     },
-    template: function(h, { row, $index }) {
+    template: function(h, { $index }) {
       return <div>{ $index + 1 }</div>;
     },
     sortable: false
@@ -117,7 +117,8 @@ export default {
     },
     fixed: [Boolean, String],
     formatter: Function,
-    selectable: Function
+    selectable: Function,
+    reserveSelection: Boolean
   },
 
   render() {},
@@ -203,6 +204,7 @@ export default {
       showTooltipWhenOverflow: this.showTooltipWhenOverflow,
       formatter: this.formatter,
       selectable: this.selectable,
+      reserveSelection: this.reserveSelection,
       fixed: this.fixed
     });
 

+ 122 - 30
packages/table/src/table-store.js

@@ -2,6 +2,15 @@ import Vue from 'vue';
 import debounce from 'throttle-debounce/debounce';
 import { orderBy } from './util';
 
+const getRowIdentity = (row, rowKey) => {
+  if (!row) throw new Error('row is required when get row identity');
+  if (typeof rowKey === 'string') {
+    return row[rowKey];
+  } else if (typeof rowKey === 'function') {
+    return rowKey.call(null, row);
+  }
+};
+
 const TableStore = function(table, initialState = {}) {
   if (!table) {
     throw new Error('Table is required.');
@@ -9,6 +18,7 @@ const TableStore = function(table, initialState = {}) {
   this.table = table;
 
   this.states = {
+    rowKey: null,
     _columns: [],
     columns: [],
     fixedColumns: [],
@@ -21,8 +31,9 @@ const TableStore = function(table, initialState = {}) {
       direction: null
     },
     isAllSelected: false,
-    selection: null,
-    allowNoSelection: false,
+    selection: [],
+    reserveSelection: false,
+    allowNoCurrentRow: false,
     selectionMode: 'none',
     selectable: null,
     currentRow: null,
@@ -43,7 +54,31 @@ TableStore.prototype.mutations = {
       data.forEach((item) => Vue.set(item, '$selected', false));
     }
     states.data = orderBy((data || []), states.sortCondition.property, states.sortCondition.direction);
-    this.updateSelectedRow();
+    this.updateCurrentRow();
+
+    if (!states.reserveSelection) {
+      states.isAllSelected = false;
+    } else {
+      const rowKey = states.rowKey;
+      if (rowKey) {
+        const selectionMap = {};
+        states.selection.forEach((row) => {
+          selectionMap[getRowIdentity(row, rowKey)] = row;
+        });
+
+        states.data.forEach((row) => {
+          const rowId = getRowIdentity(row, rowKey);
+          if (selectionMap[rowId]) {
+            row.$selected = true;
+            selectionMap[rowId] = row;
+          }
+        });
+
+        this.updateAllSelected();
+      } else {
+        console.warn('WARN: rowKey is required when reserve-selection is enabled.');
+      }
+    }
 
     if (states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0) Vue.nextTick(() => this.table.syncHeight());
     Vue.nextTick(() => this.table.updateScrollY());
@@ -65,6 +100,7 @@ TableStore.prototype.mutations = {
     }
     if (column.type === 'selection') {
       states.selectable = column.selectable;
+      states.reserveSelection = column.reserveSelection;
     }
 
     this.scheduleLayout();
@@ -83,38 +119,61 @@ TableStore.prototype.mutations = {
     states.hoverRow = row;
   },
 
-  rowSelectedChanged(states) {
-    let isAllSelected = true;
-    const data = states.data || [];
-    for (let i = 0, j = data.length; i < j; i++) {
-      const item = data[i];
-      if (states.selectable) {
-        if (states.selectable.call(null, item, i) && !item.$selected) {
-          isAllSelected = false;
-          break;
-        }
-      } else {
-        if (!item.$selected) {
-          isAllSelected = false;
-          break;
-        }
+  rowSelectedChanged(states, row) {
+    const selection = states.selection;
+    if (row.$selected) {
+      if (selection.indexOf(row) === -1) {
+        selection.push(row);
+      }
+    } else {
+      const index = selection.indexOf(row);
+      if (index > -1) {
+        selection.splice(index, 1);
       }
     }
-    states.isAllSelected = isAllSelected;
+    this.table.$emit('selection-change', selection);
+    this.table.$emit('select', selection, row);
+
+    this.updateAllSelected();
   },
 
   toggleAllSelection: debounce(10, function(states) {
     const data = states.data || [];
     const value = !states.isAllSelected;
+    const selection = this.states.selection;
+    let selectionChanged = false;
+
+    const setSelected = (item) => {
+      if (item.$selected !== value) {
+        selectionChanged = true;
+        if (value) {
+          if (selection.indexOf(item) === -1) {
+            selection.push(item);
+          }
+        } else {
+          const itemIndex = selection.indexOf(item);
+          if (itemIndex > -1) {
+            selection.splice(itemIndex, 1);
+          }
+        }
+      }
+      item.$selected = value;
+    };
+
     data.forEach((item, index) => {
       if (states.selectable) {
         if (states.selectable.call(null, item, index)) {
-          item.$selected = value;
+          setSelected(item);
         }
       } else {
-        item.$selected = value;
+        setSelected(item);
       }
     });
+
+    if (selectionChanged) {
+      this.table.$emit('selection-change', selection);
+    }
+    this.table.$emit('select-all', selection);
     states.isAllSelected = value;
   }),
 
@@ -138,25 +197,58 @@ TableStore.prototype.updateColumns = function() {
   states.columns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
 };
 
-TableStore.prototype.updateSelectedRow = function() {
+TableStore.prototype.clearSelection = function() {
+  const states = this.states;
+  const oldSelection = states.selection;
+  oldSelection.forEach((row) => { row.$selected = false; });
+  if (this.states.reserveSelection) {
+    const data = states.data || [];
+    data.forEach((row) => { row.$selected = false; });
+  }
+  states.isAllSelected = false;
+  states.selection = [];
+};
+
+TableStore.prototype.updateAllSelected = function() {
+  const states = this.states;
+  let isAllSelected = true;
+  const data = states.data || [];
+  for (let i = 0, j = data.length; i < j; i++) {
+    const item = data[i];
+    if (states.selectable) {
+      if (states.selectable.call(null, item, i) && !item.$selected) {
+        isAllSelected = false;
+        break;
+      }
+    } else {
+      if (!item.$selected) {
+        isAllSelected = false;
+        break;
+      }
+    }
+  }
+  states.isAllSelected = isAllSelected;
+};
+
+TableStore.prototype.updateCurrentRow = function() {
   const states = this.states;
   const table = this.table;
   const data = states.data || [];
   if (states.selectionMode === 'single') {
-    const oldSelectedRow = states.currentRow;
-    if (oldSelectedRow === null && !states.allowNoSelection) {
+    const oldCurrentRow = states.currentRow;
+    if (oldCurrentRow === null && !states.allowNoCurrentRow) {
       states.currentRow = data[0];
-      if (states.currentRow !== oldSelectedRow) {
-        table.$emit('selectionchange', states.currentRow);
+      if (states.currentRow !== oldCurrentRow) {
+        table.$emit('selection-change', states.currentRow);
       }
-    } else if (data.indexOf(oldSelectedRow) === -1) {
-      if (!states.allowNoSelection) {
+    } else if (data.indexOf(oldCurrentRow) === -1) {
+      if (!states.allowNoCurrentRow) {
         states.currentRow = data[0];
       } else {
         states.currentRow = null;
       }
-      if (states.currentRow !== oldSelectedRow) {
-        table.$emit('selectionchange', states.currentRow);
+      if (states.currentRow !== oldCurrentRow) {
+        table.$emit('selection-change', states.currentRow);
       }
     }
   }

+ 11 - 9
packages/table/src/table.vue

@@ -123,7 +123,9 @@
         default: 'none'
       },
 
-      allowNoSelection: Boolean,
+      rowKey: [String, Function],
+
+      allowNoCurrentRow: Boolean,
 
       rowClassName: [String, Function]
     },
@@ -134,6 +136,10 @@
     },
 
     methods: {
+      clearSelection() {
+        this.store.clearSelection();
+      },
+
       handleMouseLeave() {
         this.store.commit('setHoverRow', null);
         if (this.hoverState) this.hoverState = null;
@@ -192,8 +198,7 @@
 
       selection() {
         if (this.selectionMode === 'multiple') {
-          const data = this.tableData || [];
-          return data.filter(item => item.$selected === true);
+          return this.store.selection;
         } else if (this.selectionMode === 'single') {
           return this.store.currentRow;
         }
@@ -218,10 +223,6 @@
     },
 
     watch: {
-      selection(val) {
-        this.$emit('selectionchange', val);
-      },
-
       height(value) {
         this.layout.setHeight(value);
       },
@@ -247,8 +248,9 @@
 
     data() {
       const store = new TableStore(this, {
-        allowNoSelection: this.allowNoSelection,
-        selectionMode: this.selectionMode
+        allowNoCurrentRow: this.allowNoCurrentRow,
+        selectionMode: this.selectionMode,
+        rowKey: this.rowKey
       });
       const layout = new TableLayout({
         store,