Explorar o código

add upload limit & form validate return promise (#7405)

* Carbon: upload limit & input append & form validate promise

* Update upload.md

* Update upload.md

* Update index.js
Black Wayne %!s(int64=7) %!d(string=hai) anos
pai
achega
5426c957a2

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

@@ -324,7 +324,7 @@ Prepend or append an element, generally a label or a button.
       <el-option label="Order No." value="2"></el-option>
       <el-option label="Tel" value="3"></el-option>
     </el-select>
-    <el-button slot="append" icon="search"></el-button>
+    <el-button slot="append" icon="el-icon-search"></el-button>
   </el-input>
 </div>
 

+ 12 - 1
examples/docs/en-US/upload.md

@@ -113,6 +113,9 @@
       },
       handleChange(file, fileList) {
         this.fileList3 = fileList.slice(-3);
+      },
+      handleExceed(files, fileList) {
+        this.$message.warning(`You can upload up to 3 files. You selected ${files.length} files this time, and ${files.length + fileList.length} files totally`);
       }
     }
   }
@@ -123,13 +126,16 @@ Upload files by clicking or drag-and-drop
 
 ### Click to upload files
 
-:::demo Customize upload button type and text using `slot`.
+:::demo Customize upload button type and text using `slot`. Set `limit` and `on-exceed` to limit the maximum number of uploads allowed and specify method when the limit is exceeded.
 ```html
 <el-upload
   class="upload-demo"
   action="https://jsonplaceholder.typicode.com/posts/"
   :on-preview="handlePreview"
   :on-remove="handleRemove"
+  multiple
+  :limit="3"
+  :on-exceed="handleExceed"
   :file-list="fileList">
   <el-button size="small" type="primary">Click to upload</el-button>
   <div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
@@ -147,6 +153,9 @@ Upload files by clicking or drag-and-drop
       },
       handlePreview(file) {
         console.log(file);
+      },
+      handleExceed(files, fileList) {
+        this.$message.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + fileList.length} totally`);
       }
     }
   }
@@ -407,6 +416,8 @@ list-type | type of fileList | string | text/picture/picture-card | text |
 auto-upload | whether to auto upload file | boolean | — | true |
 http-request | override default xhr behavior, allowing you to implement your own upload-file's request | function | — | — |
 disabled | whether to disable upload | boolean | — | false |
+limit | maximum number of uploads allowed | number | — | — |
+on-exceed | hook function when limit is exceeded | function(files, fileList) | — | - |
 
 ### Methods
 | Methods Name | Description | Parameters |

+ 1 - 1
examples/docs/zh-CN/input.md

@@ -350,7 +350,7 @@ export default {
       <el-option label="订单号" value="2"></el-option>
       <el-option label="用户电话" value="3"></el-option>
     </el-select>
-    <el-button slot="append" icon="search"></el-button>
+    <el-button slot="append" icon="el-icon-search"></el-button>
   </el-input>
 </div>
 <style>

+ 25 - 14
examples/docs/zh-CN/upload.md

@@ -112,6 +112,9 @@
       },
       handleChange(file, fileList) {
         this.fileList3 = fileList.slice(-3);
+      },
+      handleExceed(files, fileList) {
+        this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
       }
     }
   }
@@ -123,13 +126,16 @@
 
 ### 点击上传
 
-::: demo 通过 slot 你可以传入自定义的上传按钮类型和文字提示。
+::: demo 通过 slot 你可以传入自定义的上传按钮类型和文字提示。可通过设置 `limit` 和 `on-exceed` 来限制上传文件的个数和定义超出限制时的行为。 
 ```html
 <el-upload
   class="upload-demo"
   action="https://jsonplaceholder.typicode.com/posts/"
   :on-preview="handlePreview"
   :on-remove="handleRemove"
+  multiple
+  :limit="3"
+  :on-exceed="handleExceed"
   :file-list="fileList">
   <el-button size="small" type="primary">点击上传</el-button>
   <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
@@ -147,6 +153,9 @@
       },
       handlePreview(file) {
         console.log(file);
+      },
+      handleExceed(files, fileList) {
+        this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
       }
     }
   }
@@ -394,27 +403,29 @@
 ### Attribute
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
-| action | 必选参数, 上传的地址 | string | — | — |
-| headers | 可选参数, 设置上传的请求头部 | object | — | — |
-| multiple | 可选参数, 是否支持多选文件 | boolean | — | — |
-| data | 可选参数, 上传时附带的额外参数 | object | — | — |
-| name | 可选参数, 上传的文件字段名 | string | — | file |
+| action | 必选参数上传的地址 | string | — | — |
+| headers | 设置上传的请求头部 | object | — | — |
+| multiple | 是否支持多选文件 | boolean | — | — |
+| data | 上传时附带的额外参数 | object | — | — |
+| name | 上传的文件字段名 | string | — | file |
 | with-credentials | 支持发送 cookie 凭证信息 | boolean | — | false |
 | show-file-list | 是否显示已上传文件列表 | boolean | — | true |
 | drag | 是否启用拖拽上传 | boolean | — | false |
-| accept | 可选参数, 接受上传的[文件类型](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept)(thumbnail-mode 模式下此参数无效)| string | — | — |
-| on-preview | 可选参数, 点击已上传的文件链接时的钩子, 可以通过 file.response 拿到服务端返回数据 | function(file) | — | — |
-| on-remove | 可选参数, 文件列表移除文件时的钩子 | function(file, fileList) | — | — |
-| on-success | 可选参数, 文件上传成功时的钩子 | function(response, file, fileList) | — | — |
-| on-error | 可选参数, 文件上传失败时的钩子 | function(err, file, fileList) | — | — |
-| on-progress | 可选参数, 文件上传时的钩子 | function(event, file, fileList) | — | — |
-| on-change | 可选参数, 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 | function(file, fileList) | — | — |
-| before-upload | 可选参数, 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 | function(file) | — | — |
+| accept | 接受上传的[文件类型](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept)(thumbnail-mode 模式下此参数无效)| string | — | — |
+| on-preview | 点击已上传的文件链接时的钩子, 可以通过 file.response 拿到服务端返回数据 | function(file) | — | — |
+| on-remove | 文件列表移除文件时的钩子 | function(file, fileList) | — | — |
+| on-success | 文件上传成功时的钩子 | function(response, file, fileList) | — | — |
+| on-error | 文件上传失败时的钩子 | function(err, file, fileList) | — | — |
+| on-progress | 文件上传时的钩子 | function(event, file, fileList) | — | — |
+| on-change | 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 | function(file, fileList) | — | — |
+| before-upload | 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 | function(file) | — | — |
 | list-type | 文件列表的类型 | string | text/picture/picture-card | text |
 | auto-upload | 是否在选取文件后立即进行上传 | boolean | — | true |
 | file-list | 上传的文件列表, 例如: [{name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'}] | array | — | [] |
 | http-request | 覆盖默认的上传行为,可以自定义上传的实现 | function | — | — |
 | disabled | 是否禁用 | boolean | — | false |
+| limit | 最大允许上传个数 |  number | — | — |
+| on-exceed | 文件超出个数限制时的钩子 | function(files, fileList) | — | - |
 
 ### Methods
 | 方法名      | 说明          | 参数 |

+ 8 - 6
packages/form/src/form.vue

@@ -73,9 +73,8 @@
         if (!this.model) {
           console.warn('[Element Warn][Form]model is required for validate to work!');
           return;
-        };
+        }
         let valid = true;
-        let count = 0;
         // 如果需要验证的fields为空,调用验证时立刻返回callback
         if (this.fields.length === 0 && callback) {
           callback(true);
@@ -85,14 +84,17 @@
             if (errors) {
               valid = false;
             }
-            if (typeof callback === 'function' && ++count === this.fields.length) {
-              callback(valid);
-            }
           });
         });
+
+        if (typeof callback === 'function') {
+          callback(valid);
+        } else if (window.Promise) {
+          return Promise[valid ? 'resolve' : 'reject'](valid); // eslint-disable-line
+        }
       },
       validateField(prop, cb) {
-        var field = this.fields.filter(field => field.prop === prop)[0];
+        let field = this.fields.filter(field => field.prop === prop)[0];
         if (!field) { throw new Error('must call validateField with valid prop string!'); }
 
         field.validate('', cb);

+ 24 - 3
packages/input/src/input.vue

@@ -30,7 +30,7 @@
         :aria-label="label"
       >
       <!-- 前置内容 -->
-      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
+      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon" :style="prefixOffset">
         <slot name="prefix"></slot>
         <i class="el-input__icon"
            v-if="prefixIcon"
@@ -38,7 +38,7 @@
         </i>
       </span>
       <!-- 后置内容 -->
-      <span class="el-input__suffix" v-if="$slots.suffix || suffixIcon || validateState">
+      <span class="el-input__suffix" v-if="$slots.suffix || suffixIcon || validateState" :style="suffixOffset">
         <span class="el-input__suffix-inner">
           <slot name="suffix"></slot>
           <i class="el-input__icon"
@@ -90,7 +90,9 @@
     data() {
       return {
         currentValue: this.value,
-        textareaCalcStyle: {}
+        textareaCalcStyle: {},
+        prefixOffset: null,
+        suffixOffset: null
       };
     },
 
@@ -149,6 +151,9 @@
       },
       textareaStyle() {
         return merge({}, this.textareaCalcStyle, { resize: this.resize });
+      },
+      isGroup() {
+        return this.$slots.prepend || this.$slots.append;
       }
     },
 
@@ -197,6 +202,18 @@
         if (this.validateEvent) {
           this.dispatch('ElFormItem', 'el.form.change', [value]);
         }
+      },
+      calcIconOffset(place) {
+        const pendantMap = {
+          'suf': 'append',
+          'pre': 'prepend'
+        };
+
+        const pendant = pendantMap[place];
+
+        if (this.$slots[pendant]) {
+          return { transform: `translateX(${place === 'suf' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)` };
+        }
       }
     },
 
@@ -206,6 +223,10 @@
 
     mounted() {
       this.resizeTextarea();
+      if (this.isGroup) {
+        this.prefixOffset = this.calcIconOffset('pre');
+        this.suffixOffset = this.calcIconOffset('suf');
+      }
     }
   };
 </script>

+ 4 - 0
packages/theme-chalk/src/input.scss

@@ -178,6 +178,10 @@
     width: 1px;
     white-space: nowrap;
 
+    &:focus {
+      outline: none;
+    }
+
     .el-select,
     .el-button {
       display: block;

+ 8 - 1
packages/upload/src/iframe-upload.vue

@@ -33,7 +33,9 @@ export default {
     },
     drag: Boolean,
     listType: String,
-    disabled: Boolean
+    disabled: Boolean,
+    limit: Number,
+    onExceed: Function
   },
 
   data() {
@@ -61,6 +63,11 @@ export default {
       }
     },
     uploadFiles(file) {
+      if (this.limit && this.$parent.uploadFiles.length + file.length > this.limit) {
+        this.onExceed && this.onExceed(this.fileList);
+        return;
+      }
+
       if (this.submitting) return;
       this.submitting = true;
       this.file = file;

+ 8 - 1
packages/upload/src/index.vue

@@ -91,7 +91,12 @@ export default {
       default: 'text'   // text,picture,picture-card
     },
     httpRequest: Function,
-    disabled: Boolean
+    disabled: Boolean,
+    limit: Number,
+    onExceed: {
+      type: Function,
+      default: noop
+    }
   },
 
   data() {
@@ -239,6 +244,8 @@ export default {
         autoUpload: this.autoUpload,
         listType: this.listType,
         disabled: this.disabled,
+        limit: this.limit,
+        'on-exceed': this.onExceed,
         'on-start': this.handleStart,
         'on-progress': this.handleProgress,
         'on-success': this.handleSuccess,

+ 8 - 1
packages/upload/src/upload.vue

@@ -43,7 +43,9 @@ export default {
       type: Function,
       default: ajax
     },
-    disabled: Boolean
+    disabled: Boolean,
+    limit: Number,
+    onExceed: Function
   },
 
   data() {
@@ -64,6 +66,11 @@ export default {
       this.uploadFiles(files);
     },
     uploadFiles(files) {
+      if (this.limit && this.fileList.length + files.length > this.limit) {
+        this.onExceed && this.onExceed(files, this.fileList);
+        return;
+      }
+
       let postFiles = Array.prototype.slice.call(files);
       if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
 

+ 48 - 0
test/unit/specs/form.spec.js

@@ -4,6 +4,20 @@ const DELAY = 50;
 
 describe('Form', () => {
   let vm;
+  let hasPromise = true;
+  before(() => {
+    if (!window.Promise) {
+      hasPromise = false;
+      window.Promise = require('es6-promise').Promise;
+    }
+  });
+
+  after(() => {
+    if (!hasPromise) {
+      window.Promise = undefined;
+    }
+  });
+
   afterEach(() => {
     destroyVM(vm);
   });
@@ -670,5 +684,39 @@ describe('Form', () => {
         });
       });
     });
+    it('validate return promise', done => {
+      var checkName = (rule, value, callback) => {
+        if (value.length < 5) {
+          callback(new Error('长度至少为5'));
+        } else {
+          callback();
+        }
+      };
+      vm = createVue({
+        template: `
+          <el-form :model="form" :rules="rules" ref="form">
+            <el-form-item label="活动名称" prop="name" ref="field">
+              <el-input v-model="form.name"></el-input>
+            </el-form-item>
+          </el-form>
+        `,
+        data() {
+          return {
+            form: {
+              name: ''
+            },
+            rules: {
+              name: [
+                { validator: checkName, trigger: 'change' }
+              ]
+            }
+          };
+        }
+      }, true);
+      vm.$refs.form.validate().catch(validFailed => {
+        expect(validFailed).to.false;
+        done();
+      });
+    });
   });
 });

+ 33 - 0
test/unit/specs/upload.spec.js

@@ -101,6 +101,13 @@ describe('Upload', () => {
           if (handlers.onPreview) {
             handlers.onPreview(file);
           }
+        },
+        limit: 2,
+        onExceed(files, fileList) {
+          console.log('onExceed', files, fileList);
+          if (handlers.onExceed) {
+            handlers.onExceed(files, fileList);
+          }
         }
       }
     };
@@ -223,5 +230,31 @@ describe('Upload', () => {
         requests[0].respond(200, {}, `${files[0].name}`);
       }, 100);
     });
+
+    it('limit files', done => {
+      const files = [{
+        name: 'exceed2.png',
+        type: 'xml'
+      }, {
+        name: 'exceed3.png',
+        type: 'xml'
+      }];
+
+      uploader.uploadFiles = [{
+        name: 'exceed1.png',
+        type: 'xml'
+      }];
+
+      handlers.onExceed = (files, fileList) => {
+        uploader.$nextTick(_ => {
+          expect(uploader.uploadFiles.length).to.equal(1);
+          done();
+        });
+      };
+
+      console.log(uploader.$refs['upload-inner'].limit, uploader.$refs['upload-inner'].fileList, uploader.$refs['upload-inner'].onExceed);
+
+      uploader.$nextTick(_ => uploader.$refs['upload-inner'].handleChange({ target: { files }}));
+    });
   });
 });