diff --git a/components/upload/demo/custom-request.md b/components/upload/demo/custom-request.md
new file mode 100644
index 00000000000..3f6a0305082
--- /dev/null
+++ b/components/upload/demo/custom-request.md
@@ -0,0 +1,14 @@
+---
+order: 10
+title:
+ zh-CN: 自定义上传
+ en-US: Custom request
+---
+
+## zh-CN
+
+使用 `nzCustomRequest` 改变上传行为。
+
+## en-US
+
+Use `nzCustomRequest` override for the default xhr behavior.
diff --git a/components/upload/demo/custom-request.ts b/components/upload/demo/custom-request.ts
new file mode 100644
index 00000000000..28c01af31d3
--- /dev/null
+++ b/components/upload/demo/custom-request.ts
@@ -0,0 +1,80 @@
+import { Component } from '@angular/core';
+import { HttpRequest, HttpClient, HttpEventType, HttpEvent, HttpResponse } from '@angular/common/http';
+import { NzMessageService, UploadXHRArgs } from 'ng-zorro-antd';
+import { forkJoin } from 'rxjs';
+
+@Component({
+ selector: 'nz-demo-upload-custom-request',
+ template: `
+
+
+
+ `
+})
+export class NzDemoUploadCustomRequestComponent {
+
+ constructor(private http: HttpClient, private msg: NzMessageService) {}
+
+ customReq = (item: UploadXHRArgs) => {
+ // 构建一个 FormData 对象,用于存储文件或其他参数
+ const formData = new FormData();
+ // tslint:disable-next-line:no-any
+ formData.append('file', item.file as any);
+ formData.append('id', '1000');
+ const req = new HttpRequest('POST', item.action, formData, {
+ reportProgress : true,
+ withCredentials: true
+ });
+ // 始终返回一个 `Subscription` 对象,nz-upload 会在适当时机自动取消订阅
+ return this.http.request(req).subscribe((event: HttpEvent<{}>) => {
+ if (event.type === HttpEventType.UploadProgress) {
+ if (event.total > 0) {
+ // tslint:disable-next-line:no-any
+ (event as any).percent = event.loaded / event.total * 100;
+ }
+ // 处理上传进度条,必须指定 `percent` 属性来表示进度
+ item.onProgress(event, item.file);
+ } else if (event instanceof HttpResponse) {
+ // 处理成功
+ item.onSuccess(event.body, item.file, event);
+ }
+ }, (err) => {
+ // 处理失败
+ item.onError(err, item.file);
+ });
+ }
+
+ // 一个简单的分片上传
+ customBigReq = (item: UploadXHRArgs) => {
+ const size = item.file.size;
+ const chunkSize = parseInt((size / 3) + '', 10);
+ const maxChunk = Math.ceil(size / chunkSize);
+ const reqs = Array(maxChunk).fill(0).map((v: {}, index: number) => {
+ const start = index * chunkSize;
+ let end = start + chunkSize;
+ if (size - end < 0) {
+ end = size;
+ }
+ const formData = new FormData();
+ formData.append('file', item.file.slice(start, end));
+ formData.append('start', start.toString());
+ formData.append('end', end.toString());
+ formData.append('index', index.toString());
+ const req = new HttpRequest('POST', item.action, formData, {
+ withCredentials: true
+ });
+ return this.http.request(req);
+ });
+ return forkJoin(...reqs).subscribe(resules => {
+ // 处理成功
+ item.onSuccess({}, item.file, event);
+ }, (err) => {
+ // 处理失败
+ item.onError(err, item.file);
+ });
+ }
+}
diff --git a/components/upload/demo/directory.md b/components/upload/demo/directory.md
new file mode 100644
index 00000000000..cc0dfe57442
--- /dev/null
+++ b/components/upload/demo/directory.md
@@ -0,0 +1,14 @@
+---
+order: 6
+title:
+ zh-CN: 文件夹上传
+ en-US: Upload directory
+---
+
+## zh-CN
+
+支持上传一个文件夹里的所有文件。
+
+## en-US
+
+You can select and upload a whole directory.
diff --git a/components/upload/demo/directory.ts b/components/upload/demo/directory.ts
new file mode 100644
index 00000000000..2247388b3a1
--- /dev/null
+++ b/components/upload/demo/directory.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-upload-directory',
+ template: `
+
+
+
+ `
+})
+export class NzDemoUploadDirectoryComponent {}
diff --git a/components/upload/demo/picture-style.md b/components/upload/demo/picture-style.md
index 2d7591c1a6b..8b7310e9b39 100644
--- a/components/upload/demo/picture-style.md
+++ b/components/upload/demo/picture-style.md
@@ -1,5 +1,5 @@
---
-order: 6
+order: 8
title:
zh-CN: 图片列表样式
en-US: Pictures with list style
diff --git a/components/upload/doc/index.en-US.md b/components/upload/doc/index.en-US.md
index fd62e87f903..52a0f75e845 100644
--- a/components/upload/doc/index.en-US.md
+++ b/components/upload/doc/index.en-US.md
@@ -24,6 +24,7 @@ Uploading is the process of publishing information (web pages, text, pictures, v
| --- | --- | --- | --- |
| `[nzAccept]` | File types that can be accepted. See [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept) | string | - |
| `[nzAction]` | Required. Uploading URL | string | - |
+| `[nzDirectory]` | support upload whole directory ([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false |
| `[nzBeforeUpload]` | Hook function which will be executed before uploading. Uploading will be stopped with `false` or a Observable. **Warning:this function is not supported in IE9**. NOTICE: Muse be use `=>` to define the method. | (file, fileList) => `boolean|Observable` | - |
| `[nzCustomRequest]` | override for the default xhr behavior allowing for additional customization and ability to implement your own XMLHttpRequest. NOTICE: Muse be use `=>` to define the method. | `(item) => Subscription` | - |
| `[nzData]` | Uploading params or function which can return uploading params. NOTICE: Muse be use `=>` to define the method. | `Object|((file: UploadFile) => Object)` | - |
diff --git a/components/upload/doc/index.zh-CN.md b/components/upload/doc/index.zh-CN.md
index 1ae2e59af91..06eed84f20f 100644
--- a/components/upload/doc/index.zh-CN.md
+++ b/components/upload/doc/index.zh-CN.md
@@ -25,6 +25,7 @@ title: Upload
| --- | --- | --- | --- |
| `[nzAccept]` | 接受上传的文件类型, 详见 [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept) | string | - |
| `[nzAction]` | 必选参数, 上传的地址 | string | - |
+| `[nzDirectory]` | 支持上传文件夹([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false |
| `[nzBeforeUpload]` | 上传文件之前的钩子,参数为上传的文件,若返回 `false` 则停止上传。注意:**IE9** 不支持该方法;注意:务必使用 `=>` 定义处理方法。 | (file, fileList) => `boolean|Observable` | - |
| `[nzCustomRequest]` | 通过覆盖默认的上传行为,可以自定义自己的上传实现;注意:务必使用 `=>` 定义处理方法。 | `(item) => Subscription` | - |
| `[nzData]` | 上传所需参数或返回上传参数的方法;注意:务必使用 `=>` 定义处理方法。 | `Object|((file: UploadFile) => Object)` | - |
diff --git a/components/upload/interface.ts b/components/upload/interface.ts
index d6de3efd173..535506d2884 100644
--- a/components/upload/interface.ts
+++ b/components/upload/interface.ts
@@ -48,6 +48,7 @@ export interface ZipButtonOptions {
disabled?: boolean;
accept?: string | string[];
action?: string;
+ directory?: boolean;
beforeUpload?: (file: UploadFile, fileList: UploadFile[]) => boolean | Observable;
customRequest?: (item: any) => Subscription;
data?: {} | ((file: UploadFile) => {});
diff --git a/components/upload/nz-upload-btn.component.html b/components/upload/nz-upload-btn.component.html
index 578440292bd..9c2ecd9b739 100644
--- a/components/upload/nz-upload-btn.component.html
+++ b/components/upload/nz-upload-btn.component.html
@@ -1,3 +1,6 @@
+ [attr.accept]="options.accept"
+ [attr.directory]="options.directory ? 'directory': null"
+ [attr.webkitdirectory]="options.directory ? 'webkitdirectory': null"
+ [multiple]="options.multiple" style="display: none;">
\ No newline at end of file
diff --git a/components/upload/nz-upload-btn.component.ts b/components/upload/nz-upload-btn.component.ts
index 30971a2ca3d..00916a4ac1c 100644
--- a/components/upload/nz-upload-btn.component.ts
+++ b/components/upload/nz-upload-btn.component.ts
@@ -66,11 +66,15 @@ export class NzUploadBtnComponent implements OnInit, OnChanges, OnDestroy {
e.preventDefault();
return;
}
- const files: File[] = Array.prototype.slice.call(e.dataTransfer.files).filter(
- (file: File) => this.attrAccept(file, this.options.accept)
- );
- if (files.length) {
- this.uploadFiles(files);
+ if (this.options.directory) {
+ this.traverseFileTree(e.dataTransfer.items);
+ } else {
+ const files: File[] = Array.prototype.slice.call(e.dataTransfer.files).filter(
+ (file: File) => this.attrAccept(file, this.options.accept)
+ );
+ if (files.length) {
+ this.uploadFiles(files);
+ }
}
e.preventDefault();
@@ -85,6 +89,31 @@ export class NzUploadBtnComponent implements OnInit, OnChanges, OnDestroy {
hie.value = '';
}
+ // tslint:disable-next-line:no-any
+ private traverseFileTree(files: any): void {
+ // tslint:disable-next-line:no-any
+ const _traverseFileTree = (item: any, path: string) => {
+ if (item.isFile) {
+ item.file((file) => {
+ if (this.attrAccept(file, this.options.accept)) {
+ this.uploadFiles([file]);
+ }
+ });
+ } else if (item.isDirectory) {
+ const dirReader = item.createReader();
+
+ dirReader.readEntries((entries) => {
+ for (const entrieItem of entries) {
+ _traverseFileTree(entrieItem, `${path}${item.name}/`);
+ }
+ });
+ }
+ };
+ for (const file of files) {
+ _traverseFileTree(file.webkitGetAsEntry(), '');
+ }
+ }
+
private attrAccept(file: File, acceptedFiles: string | string[]): boolean {
if (file && acceptedFiles) {
const acceptedFilesArray = Array.isArray(acceptedFiles) ? acceptedFiles : acceptedFiles.split(',');
@@ -133,7 +162,7 @@ export class NzUploadBtnComponent implements OnInit, OnChanges, OnDestroy {
if (processedFileType === '[object File]' || processedFileType === '[object Blob]') {
this.attachUid(processedFile);
this.post(processedFile);
- } else {
+ } else if (typeof processedFile === 'boolean' && processedFile !== false) {
this.post(file);
}
});
diff --git a/components/upload/nz-upload.component.ts b/components/upload/nz-upload.component.ts
index 86b45c1f094..38bab695772 100644
--- a/components/upload/nz-upload.component.ts
+++ b/components/upload/nz-upload.component.ts
@@ -16,7 +16,7 @@ import {
import { of, Observable, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
-import { toBoolean, toNumber } from '../core/util/convert';
+import { toBoolean, toNumber, InputBoolean } from '../core/util/convert';
import { NzI18nService } from '../i18n/nz-i18n.service';
import {
@@ -39,8 +39,6 @@ import { NzUploadBtnComponent } from './nz-upload-btn.component';
export class NzUploadComponent implements OnInit, OnChanges, OnDestroy {
private i18n$: Subscription;
locale: any = {};
- private inited = false;
- private progressTimer: any;
@ViewChild('upload') upload: NzUploadBtnComponent;
// region: fields
@@ -70,6 +68,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy {
@Input() nzFileType: string;
@Input() nzAccept: string | string[];
@Input() nzAction: string;
+ @Input() @InputBoolean() nzDirectory: boolean = false;
@Input() nzBeforeUpload: (file: UploadFile, fileList: UploadFile[]) => boolean | Observable;
@Input() nzCustomRequest: (item: any) => Subscription;
@Input() nzData: {} | ((file: UploadFile) => {});
@@ -176,6 +175,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy {
disabled : this.nzDisabled,
accept : this.nzAccept,
action : this.nzAction,
+ directory : this.nzDirectory,
beforeUpload : this.nzBeforeUpload,
customRequest : this.nzCustomRequest,
data : this.nzData,
@@ -240,7 +240,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy {
file.thumbUrl = '';
const reader = new FileReader();
- reader.onloadend = () => file.thumbUrl = reader.result;
+ reader.onloadend = () => file.thumbUrl = reader.result as string;
reader.readAsDataURL(file.originFileObj);
}
@@ -361,7 +361,6 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy {
// endregion
ngOnInit(): void {
- this.inited = true;
this.i18n$ = this.i18n.localeChange.subscribe(() => {
this.locale = this.i18n.getLocaleData('Upload');
this.cd.detectChanges();
diff --git a/components/upload/upload.spec.ts b/components/upload/upload.spec.ts
index 2da82e3162a..564f9df7563 100644
--- a/components/upload/upload.spec.ts
+++ b/components/upload/upload.spec.ts
@@ -19,10 +19,12 @@ import { NzUploadBtnComponent } from './nz-upload-btn.component';
import { NzUploadListComponent } from './nz-upload-list.component';
import { NzUploadComponent } from './nz-upload.component';
-const PNGSMALL = { target: { files: [new File([`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`], 'test.png', {
+const FILECONTENT = [`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`];
+const FILE = new File(FILECONTENT, null, { type: null });
+const PNGSMALL = { target: { files: [new File(FILECONTENT, 'test.png', {
type: 'image/png'
})] } };
-const JPGSMALL = { target: { files: [new File([`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`], 'test.jpg', {
+const JPGSMALL = { target: { files: [new File(FILECONTENT, 'test.jpg', {
type: 'image/jpg'
})] } };
const LARGEFILE = {
@@ -66,12 +68,7 @@ describe('upload', () => {
it('should be upload a file', () => {
expect(instance._nzChange).toBeUndefined();
- pageObject.postFile(
- new File(
- [`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`],
- null, { type: null }
- )
- );
+ pageObject.postFile(FILE);
const req = httpMock.expectOne(instance.nzAction);
pageObject.expectChange();
req.flush({});
@@ -325,6 +322,15 @@ describe('upload', () => {
pageObject.postSmall();
expect(ret).toBe(true);
});
+ it('cancel upload when returan a false value', () => {
+ expect(instance._nzChange).toBeUndefined();
+ instance.beforeUpload = (file: UploadFile, fileList: UploadFile[]): Observable => {
+ return of(false);
+ };
+ fixture.detectChanges();
+ pageObject.postSmall();
+ expect(instance._nzChange).toBeUndefined();
+ });
});
});
@@ -379,12 +385,7 @@ describe('upload', () => {
instance.nzFileList = null;
fixture.detectChanges();
expect(instance._nzChange).toBeUndefined();
- pageObject.postFile(
- new File(
- [`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`],
- null, { type: null }
- )
- );
+ pageObject.postFile(FILE);
const req = httpMock.expectOne(instance.nzAction);
pageObject.expectChange();
req.flush({});
@@ -670,12 +671,7 @@ describe('upload', () => {
expect(instance.comp.uploadFiles).not.toHaveBeenCalled();
instance.comp.onFileDrop({
type: 'dragend',
- dataTransfer: { files: [
- new File(
- [`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==`],
- null, { type: null }
- )
- ] },
+ dataTransfer: { files: [ FILE ] },
preventDefault: () => {}
} as any);
expect(instance.comp.uploadFiles).toHaveBeenCalled();
@@ -725,6 +721,76 @@ describe('upload', () => {
instance.comp.onChange(PNGSMALL as any);
expect(instance.comp.uploadFiles).toHaveBeenCalled();
});
+ describe('via directory', () => {
+ // tslint:disable:no-invalid-this
+ // tslint:disable-next-line:typedef
+ function Item(name) {
+ this.name = name;
+ this.toString = () => this.name;
+ }
+ const makeFileSystemEntry = (item) => {
+ const isDirectory = Array.isArray(item.children);
+ const ret = {
+ isDirectory,
+ isFile: !isDirectory,
+ file: (handle) => {
+ handle(new Item(item.name));
+ },
+ createReader: () => {
+ return {
+ readEntries: handle => handle(item.children.map(makeFileSystemEntry))
+ };
+ }
+ };
+ return ret;
+ };
+ const makeDataTransferItem = (item) => ({ webkitGetAsEntry: () => makeFileSystemEntry(item) });
+ beforeEach(() => instance.options.directory = true);
+ it('should working', () => {
+ spyOn(instance.comp, 'uploadFiles');
+ const files = {
+ name: 'foo',
+ children: [
+ {
+ name: 'bar',
+ children: [
+ {
+ name: 'is.webp'
+ }
+ ]
+ }
+ ]
+ };
+ instance.comp.onFileDrop({
+ type: 'dragend',
+ dataTransfer: {
+ items: [makeDataTransferItem(files)]
+ },
+ preventDefault: () => {}
+ } as any);
+ expect(instance.comp.uploadFiles).toHaveBeenCalled();
+ });
+ it('should be ingore invalid extension', () => {
+ instance.options.accept = ['.webp'];
+ spyOn(instance.comp, 'uploadFiles');
+ const files = {
+ name: 'foo',
+ children: [
+ {
+ name: 'is.jpg'
+ }
+ ]
+ };
+ instance.comp.onFileDrop({
+ type: 'dragend',
+ dataTransfer: {
+ items: [makeDataTransferItem(files)]
+ },
+ preventDefault: () => {}
+ } as any);
+ expect(instance.comp.uploadFiles).not.toHaveBeenCalled();
+ });
+ });
});
describe('should be disabled upload', () => {
@@ -908,6 +974,7 @@ describe('upload', () => {
[nzWithCredentials]="nzWithCredentials"
[nzPreview]="onPreview"
[nzRemove]="onRemove"
+ [nzDirectory]="directory"
(nzFileListChange)="nzFileListChange($event)"
(nzChange)="nzChange($event)">