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)">