Explorar o código

Cascader: update (#2845)

* Cascader: update

* Cascader: add tests

* Cascader: move flatOptions and add debounce
杨奕 %!s(int64=8) %!d(string=hai) anos
pai
achega
0643460b28

+ 5 - 1
examples/components/demo-block.vue

@@ -185,7 +185,8 @@
           panel_js: 3,
           panel_css: 1
         };
-        const form = document.createElement('form');
+        const form = document.getElementById('fiddle-form') || document.createElement('form');
+        form.innerHTML = '';
         const node = document.createElement('textarea');
 
         form.method = 'post';
@@ -197,6 +198,9 @@
           node.value = data[name].toString();
           form.appendChild(node.cloneNode());
         }
+        form.setAttribute('id', 'fiddle-form');
+        form.style.display = 'none';
+        document.body.appendChild(form);
 
         form.submit();
       }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 814 - 146
examples/docs/en-US/cascader.md


+ 1 - 1
examples/docs/en-US/select.md

@@ -462,7 +462,7 @@ Display options in groups.
 
 You can filter options for your desired ones.
 
-:::demo Adding `filterable` to `el-select` enables filtering. By default, Select will find all the options whose `label` attribute contains the input value. If you prefer other filtering strategies, you can pass the `filter-method`. `filter-method` is a `Function` that gets called when the input value changed, and its parameter is the current input value.
+:::demo Adding `filterable` to `el-select` enables filtering. By default, Select will find all the options whose `label` attribute contains the input value. If you prefer other filtering strategies, you can pass the `filter-method`. `filter-method` is a `Function` that gets called when the input value changes, and its parameter is the current input value.
 ```html
 <template>
   <el-select v-model="value8" filterable placeholder="Select">

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 822 - 127
examples/docs/zh-CN/cascader.md


+ 1 - 1
examples/entry.js

@@ -1,4 +1,4 @@
-require('offline-plugin/runtime').install();
+process.env.NODE_ENV === 'production' && require('offline-plugin/runtime').install();
 
 import Vue from 'vue';
 import entry from './app';

+ 1 - 1
examples/nav.config.json

@@ -78,7 +78,7 @@
             },
             {
               "path": "/cascader",
-              "title": "Cascader 级联选择"
+              "title": "Cascader 级联选择"
             },
             {
               "path": "/switch",

+ 119 - 25
packages/cascader/src/main.vue

@@ -17,9 +17,9 @@
     <el-input
       ref="input"
       :readonly="!filterable"
-      :placeholder="displayValue ? undefined : placeholder"
+      :placeholder="currentLabels.length ? undefined : placeholder"
       v-model="inputValue"
-      @change="handleInputChange"
+      @change="debouncedInputChange"
       :validate-event="false"
       :size="size"
       :disabled="disabled"
@@ -27,7 +27,7 @@
       <template slot="icon">
         <i
           key="1"
-          v-if="inputHover && displayValue !== ''"
+          v-if="clearable && inputHover && currentLabels.length"
           class="el-input__icon el-icon-circle-close el-cascader__clearIcon"
           @click="clearValue"
         ></i>
@@ -39,7 +39,17 @@
         ></i>
       </template>
     </el-input>
-    <span class="el-cascader__label" v-show="inputValue === ''">{{displayValue}}</span>
+    <span class="el-cascader__label" v-show="inputValue === ''">
+      <template v-if="showAllLevels">
+        <template v-for="(label, index) in currentLabels">
+          {{ label }}
+          <span v-if="index < currentLabels.length - 1"> / </span>
+        </template>
+      </template>
+      <template v-else>
+        {{ currentLabels[currentLabels.length - 1] }}
+      </template>
+    </span>
   </span>
 </template>
 
@@ -51,6 +61,8 @@ import Popper from 'element-ui/src/utils/vue-popper';
 import Clickoutside from 'element-ui/src/utils/clickoutside';
 import emitter from 'element-ui/src/mixins/emitter';
 import Locale from 'element-ui/src/mixins/locale';
+import { t } from 'element-ui/src/locale';
+import debounce from 'throttle-debounce/debounce';
 
 const popperMixin = {
   props: {
@@ -84,17 +96,33 @@ export default {
       type: Array,
       required: true
     },
+    props: {
+      type: Object,
+      default() {
+        return {
+          children: 'children',
+          label: 'label',
+          value: 'value',
+          disabled: 'disabled'
+        };
+      }
+    },
     value: {
       type: Array,
       default() {
         return [];
       }
     },
-    placeholder: String,
+    placeholder: {
+      type: String,
+      default() {
+        return t('el.cascader.placeholder');
+      }
+    },
     disabled: Boolean,
     clearable: {
       type: Boolean,
-      default: true
+      default: false
     },
     changeOnSelect: Boolean,
     popperClass: String,
@@ -103,20 +131,53 @@ export default {
       default: 'click'
     },
     filterable: Boolean,
-    size: String
+    size: String,
+    showAllLevels: {
+      type: Boolean,
+      default: true
+    },
+    debounce: {
+      type: Number,
+      default: 300
+    }
   },
 
   data() {
     return {
       currentValue: this.value,
-      displayValue: this.value.join('/'),
+      menu: null,
+      debouncedInputChange() {},
       menuVisible: false,
       inputHover: false,
       inputValue: '',
-      flatOptions: this.filterable && this.flattenOptions(this.options)
+      flatOptions: null
     };
   },
 
+  computed: {
+    labelKey() {
+      return this.props.label || 'label';
+    },
+    valueKey() {
+      return this.props.value || 'value';
+    },
+    childrenKey() {
+      return this.props.children || 'children';
+    },
+    currentLabels() {
+      let options = this.options;
+      let labels = [];
+      this.currentValue.forEach(value => {
+        const targetOption = options && options.filter(option => option[this.valueKey] === value)[0];
+        if (targetOption) {
+          labels.push(targetOption[this.labelKey]);
+          options = targetOption[this.childrenKey];
+        }
+      });
+      return labels;
+    }
+  },
+
   watch: {
     menuVisible(value) {
       value ? this.showMenu() : this.hideMenu();
@@ -125,29 +186,40 @@ export default {
       this.currentValue = value;
     },
     currentValue(value) {
-      this.displayValue = value.join('/');
       this.dispatch('ElFormItem', 'el.form.change', [value]);
     },
-    options(value) {
-      this.menu.options = value;
+    options: {
+      deep: true,
+      handler(value) {
+        if (!this.menu) {
+          this.initMenu();
+        }
+        this.flatOptions = this.flattenOptions(this.options);
+        this.menu.options = value;
+      }
     }
   },
 
   methods: {
+    initMenu() {
+      this.menu = new Vue(ElCascaderMenu).$mount();
+      this.menu.options = this.options;
+      this.menu.props = this.props;
+      this.menu.expandTrigger = this.expandTrigger;
+      this.menu.changeOnSelect = this.changeOnSelect;
+      this.menu.popperClass = this.popperClass;
+      this.popperElm = this.menu.$el;
+      this.menu.$on('pick', this.handlePick);
+      this.menu.$on('activeItemChange', this.handleActiveItemChange);
+    },
     showMenu() {
       if (!this.menu) {
-        this.menu = new Vue(ElCascaderMenu).$mount();
-        this.menu.options = this.options;
-        this.menu.expandTrigger = this.expandTrigger;
-        this.menu.changeOnSelect = this.changeOnSelect;
-        this.menu.popperClass = this.popperClass;
-        this.popperElm = this.menu.$el;
+        this.initMenu();
       }
 
       this.menu.value = this.currentValue.slice(0);
       this.menu.visible = true;
       this.menu.options = this.options;
-      this.menu.$on('pick', this.handlePick);
       this.updatePopper();
       this.$nextTick(_ => {
         this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
@@ -157,6 +229,12 @@ export default {
       this.inputValue = '';
       this.menu.visible = false;
     },
+    handleActiveItemChange(value) {
+      this.$nextTick(_ => {
+        this.updatePopper();
+      });
+      this.$emit('active-item-change', value);
+    },
     handlePick(value, close = true) {
       this.currentValue = value;
       this.$emit('input', value);
@@ -176,14 +254,14 @@ export default {
       }
 
       let filteredFlatOptions = flatOptions.filter(optionsStack => {
-        return optionsStack.some(option => option.label.indexOf(value) > -1);
+        return optionsStack.some(option => new RegExp(value, 'i').test(option[this.labelKey]));
       });
 
       if (filteredFlatOptions.length > 0) {
         filteredFlatOptions = filteredFlatOptions.map(optionStack => {
           return {
             __IS__FLAT__OPTIONS: true,
-            value: optionStack.map(item => item.value),
+            value: optionStack.map(item => item[this.valueKey]),
             label: this.renderFilteredOptionLabel(value, optionStack)
           };
         });
@@ -198,8 +276,11 @@ export default {
       this.menu.options = filteredFlatOptions;
     },
     renderFilteredOptionLabel(inputValue, optionsStack) {
-      return optionsStack.map(({ label }, index) => {
-        const node = label.indexOf(inputValue) > -1 ? this.highlightKeyword(label, inputValue) : label;
+      return optionsStack.map((option, index) => {
+        const label = option[this.labelKey];
+        const keywordIndex = label.toLowerCase().indexOf(inputValue.toLowerCase());
+        const labelPart = label.slice(keywordIndex, inputValue.length + keywordIndex);
+        const node = keywordIndex > -1 ? this.highlightKeyword(label, labelPart) : label;
         return index === 0 ? node : [' / ', node];
       });
     },
@@ -215,10 +296,13 @@ export default {
       let flatOptions = [];
       options.forEach((option) => {
         const optionsStack = ancestor.concat(option);
-        if (!option.children) {
+        if (!option[this.childrenKey]) {
           flatOptions.push(optionsStack);
         } else {
-          flatOptions = flatOptions.concat(this.flattenOptions(option.children, optionsStack));
+          if (this.changeOnSelect) {
+            flatOptions.push(optionsStack);
+          }
+          flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack));
         }
       });
       return flatOptions;
@@ -238,6 +322,16 @@ export default {
       }
       this.menuVisible = !this.menuVisible;
     }
+  },
+
+  created() {
+    this.debouncedInputChange = debounce(this.debounce, value => {
+      this.handleInputChange(value);
+    });
+  },
+
+  mounted() {
+    this.flatOptions = this.flattenOptions(this.options);
   }
 };
 </script>

+ 22 - 2
packages/cascader/src/menu.vue

@@ -6,6 +6,7 @@
       return {
         inputWidth: 0,
         options: [],
+        props: {},
         visible: false,
         activeValue: [],
         value: [],
@@ -34,6 +35,20 @@
         cache: false,
         get() {
           const activeValue = this.activeValue;
+          const configurableProps = ['label', 'value', 'children', 'disabled'];
+
+          const formatOptions = options => {
+            options.forEach(option => {
+              if (option.__IS__FLAT__OPTIONS) return;
+              configurableProps.forEach(prop => {
+                const value = option[this.props[prop] || prop];
+                if (value) option[prop] = value;
+              });
+              if (Array.isArray(option.children)) {
+                formatOptions(option.children);
+              }
+            });
+          };
 
           const loadActiveOptions = (options, activeOptions = []) => {
             const level = activeOptions.length;
@@ -48,6 +63,7 @@
             return activeOptions;
           };
 
+          formatOptions(this.options);
           return loadActiveOptions(this.options);
         }
       }
@@ -66,7 +82,11 @@
         const len = this.activeOptions.length;
         this.activeValue.splice(menuIndex, len, item.value);
         this.activeOptions.splice(menuIndex + 1, len, item.children);
-        if (this.changeOnSelect) this.$emit('pick', this.activeValue, false);
+        if (this.changeOnSelect) {
+          this.$emit('pick', this.activeValue, false);
+        } else {
+          this.$emit('activeItemChange', this.activeValue);
+        }
       }
     },
 
@@ -116,7 +136,7 @@
         });
         let menuStyle = {};
         if (isFlat) {
-          menuStyle.width = this.inputWidth + 'px';
+          menuStyle.minWidth = this.inputWidth + 'px';
         }
 
         return (

+ 14 - 10
packages/theme-default/src/cascader.css

@@ -13,7 +13,7 @@
     .el-input__inner {
       cursor: pointer;
       background-color: transparent;
-      z-index: 1;
+      z-index: var(--index-normal);
     }
 
     .el-input__icon {
@@ -34,7 +34,7 @@
       top: 0;
       height: 100%;
       line-height: 34px;
-      padding: 0 15px 0 10px;
+      padding: 0 25px 0 10px;
       color: var(--input-color);
       width: 100%;
       white-space: nowrap;
@@ -42,6 +42,11 @@
       overflow: hidden;
       box-sizing: border-box;
       cursor: pointer;
+      font-size: 14px;
+      text-align: left;
+      span {
+        color: var(--color-light-silver);
+      }
     }
 
     @m large {
@@ -65,24 +70,23 @@
     background: #fff;
     position: absolute;
     margin: 5px 0;
-    z-index: 1001;
+    z-index: calc(var(--index-normal) + 1);
     border: var(--select-dropdown-border);
     border-radius: var(--border-radius-small);
-    overflow: hidden;
     box-shadow: var(--select-dropdown-shadow);
   }
 
   @b cascader-menu {
     display: inline-block;
     vertical-align: top;
-    height: 180px;
+    height: 204px;
     overflow: auto;
     border-right: var(--select-dropdown-border);
     background-color: var(--select-dropdown-background);
     box-sizing: border-box;
     margin: 0;
-    padding: 0;
-    min-width: 110px;
+    padding: 6px 0;
+    min-width: 160px;
 
     &:last-child {
       border-right: 0;
@@ -102,13 +106,13 @@
       cursor: pointer;
 
       @e keyword {
-        color: var(--color-danger);
+        font-weight: bold;
       }
       
       @m extensible {
         &:after {
           font-family: 'element-icons';
-          content: "\e602";
+          content: "\e606";
           font-size: 12px;
           transform: scale(0.8);
           color: rgb(191, 203, 217);
@@ -132,7 +136,7 @@
         color: var(--color-white);
         background-color: var(--select-option-selected);
 
-        &.hover {
+        &:hover {
           background-color: var(--select-option-selected-hover);
         }
       }

+ 1 - 1
src/locale/lang/pt.js

@@ -12,7 +12,7 @@ export default {
       startTime: 'Hora de inicio',
       endDate: 'Data de fim',
       endTime: 'Hora de fim',
-      year: 'Ano',
+      year: '',
       month1: 'Janeiro',
       month2: 'Fevereiro',
       month3: 'Março',

+ 105 - 1
test/unit/specs/cascader.spec.js

@@ -13,6 +13,7 @@ describe('Cascader', () => {
           ref="cascader"
           placeholder="请选择"
           :options="options"
+          clearable
           v-model="selectedOptions"
         ></el-cascader>
       `,
@@ -456,6 +457,7 @@ describe('Cascader', () => {
           placeholder="请选择"
           :options="options"
           filterable
+          :debounce="0"
           v-model="selectedOptions"
         ></el-cascader>
       `,
@@ -507,7 +509,7 @@ describe('Cascader', () => {
       const item1 = menuElm.querySelector('.el-cascader-menu__item');
 
       expect(menuElm.children.length).to.be.equal(1);
-      expect(menuElm.children[0].children.length).to.be.equal(1);
+      expect(menuElm.children[0].children.length).to.be.equal(3);
       done();
 
       item1.click();
@@ -521,4 +523,106 @@ describe('Cascader', () => {
       }, 500);
     }, 300);
   });
+  it('props', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          :options="options"
+          :props="props"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            label: 'Zhejiang',
+            cities: [{
+              label: 'Hangzhou'
+            }, {
+              label: 'NingBo'
+            }]
+          }, {
+            label: 'Jiangsu',
+            cities: [{
+              label: 'Nanjing'
+            }]
+          }],
+          props: {
+            value: 'label',
+            children: 'cities'
+          },
+          selectedOptions: []
+        };
+      }
+    }, true);
+    vm.$el.click();
+    setTimeout(_ => {
+      expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
+
+      const menu = vm.$refs.cascader.menu;
+      const menuElm = menu.$el;
+      let items = menuElm.querySelectorAll('.el-cascader-menu__item');
+      expect(items.length).to.equal(2);
+      items[0].click();
+      setTimeout(_ => {
+        items = menuElm.querySelectorAll('.el-cascader-menu__item');
+        expect(items.length).to.equal(4);
+        expect(items[items.length - 1].innerText).to.equal('NingBo');
+        done();
+      }, 100);
+    }, 100);
+  });
+  it('show last level', done => {
+    vm = createVue({
+      template: `
+        <el-cascader
+          ref="cascader"
+          :options="options"
+          :show-all-levels="false"
+          v-model="selectedOptions"
+        ></el-cascader>
+      `,
+      data() {
+        return {
+          options: [{
+            value: 'zhejiang',
+            label: 'Zhejiang',
+            children: [{
+              value: 'hangzhou',
+              label: 'Hangzhou',
+              children: [{
+                value: 'xihu',
+                label: 'West Lake'
+              }]
+            }, {
+              value: 'ningbo',
+              label: 'NingBo',
+              children: [{
+                value: 'jiangbei',
+                label: 'Jiang Bei'
+              }]
+            }]
+          }, {
+            value: 'jiangsu',
+            label: 'Jiangsu',
+            children: [{
+              value: 'nanjing',
+              label: 'Nanjing',
+              children: [{
+                value: 'zhonghuamen',
+                label: 'Zhong Hua Men'
+              }]
+            }]
+          }],
+          selectedOptions: ['zhejiang', 'ningbo', 'jiangbei']
+        };
+      }
+    }, true);
+    setTimeout(_ => {
+      const span = vm.$el.querySelector('.el-cascader__label');
+      expect(span.innerText).to.equal('Jiang Bei');
+      done();
+    }, 100);
+  });
 });

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio