diff --git a/components/upload/demo/avatar.ts b/components/upload/demo/avatar.ts index 789e5352b6f..6652a5f0d50 100644 --- a/components/upload/demo/avatar.ts +++ b/components/upload/demo/avatar.ts @@ -13,7 +13,7 @@ import { Observable, Observer } from 'rxjs'; [nzBeforeUpload]="beforeUpload" (nzChange)="handleChange($event)"> - +
Upload
@@ -90,16 +90,21 @@ export class NzDemoUploadAvatarComponent { } handleChange(info: { file: UploadFile }): void { - if (info.file.status === 'uploading') { - this.loading = true; - return; - } - if (info.file.status === 'done') { - // Get this url from response in real world. - this.getBase64(info.file.originFileObj, (img: string) => { + switch (info.file.status) { + case 'uploading': + this.loading = true; + break; + case 'done': + // Get this url from response in real world. + this.getBase64(info.file.originFileObj, (img: string) => { + this.loading = false; + this.avatarUrl = img; + }); + break; + case 'error': + this.msg.error('Network error'); this.loading = false; - this.avatarUrl = img; - }); + break; } } } diff --git a/components/upload/demo/manually.ts b/components/upload/demo/manually.ts index 98ef9d37066..1fb3acd9f6c 100644 --- a/components/upload/demo/manually.ts +++ b/components/upload/demo/manually.ts @@ -25,7 +25,7 @@ export class NzDemoUploadManuallyComponent { constructor(private http: HttpClient, private msg: NzMessageService) {} beforeUpload = (file: UploadFile): boolean => { - this.fileList.push(file); + this.fileList = this.fileList.concat(file); return false; } @@ -46,6 +46,7 @@ export class NzDemoUploadManuallyComponent { .subscribe( (event: {}) => { this.uploading = false; + this.fileList = []; this.msg.success('upload successfully.'); }, err => { diff --git a/components/upload/demo/picture-card.ts b/components/upload/demo/picture-card.ts index 9f87818071e..accd26f7413 100644 --- a/components/upload/demo/picture-card.ts +++ b/components/upload/demo/picture-card.ts @@ -10,6 +10,7 @@ import { NzMessageService, UploadFile } from 'ng-zorro-antd'; nzListType="picture-card" [(nzFileList)]="fileList" [nzShowButton]="fileList.length < 3" + [nzShowUploadList]="showUploadList" [nzPreview]="handlePreview">
Upload
@@ -35,6 +36,11 @@ import { NzMessageService, UploadFile } from 'ng-zorro-antd'; ] }) export class NzDemoUploadPictureCardComponent { + showUploadList = { + showPreviewIcon: true, + showRemoveIcon : true, + hidePreviewIconInNonImage: true + }; fileList = [ { uid: -1, diff --git a/components/upload/doc/index.en-US.md b/components/upload/doc/index.en-US.md index 7e54aaaf326..362e9533249 100644 --- a/components/upload/doc/index.en-US.md +++ b/components/upload/doc/index.en-US.md @@ -67,7 +67,8 @@ When uploading state change, it returns: uid: 'uid', // unique identifier name: 'xx.png' // file name status: 'done', // options:uploading, done, error, removed - response: '{"status": "success"}' // response from server + response: '{"status": "success"}', // response from server + linkProps: '{"download": "image"}', // additional html props of file link } ``` diff --git a/components/upload/doc/index.zh-CN.md b/components/upload/doc/index.zh-CN.md index f0299ddeb4c..885753294e2 100644 --- a/components/upload/doc/index.zh-CN.md +++ b/components/upload/doc/index.zh-CN.md @@ -68,7 +68,8 @@ title: Upload uid: 'uid', // 文件唯一标识 name: 'xx.png' // 文件名 status: 'done', // 状态有:uploading done error removed - response: '{"status": "success"}' // 服务端响应内容 + response: '{"status": "success"}', // 服务端响应内容 + linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性 } ``` diff --git a/components/upload/interface.ts b/components/upload/interface.ts index 2c6317bc23b..a8249995d37 100644 --- a/components/upload/interface.ts +++ b/components/upload/interface.ts @@ -25,7 +25,7 @@ export interface UploadFile { thumbUrl?: string; response?: any; error?: any; - linkProps?: any; + linkProps?: { download: string }; type: string; [ key: string ]: any; @@ -42,6 +42,7 @@ export interface UploadChangeParam { export interface ShowUploadListInterface { showRemoveIcon?: boolean; showPreviewIcon?: boolean; + hidePreviewIconInNonImage?: boolean; } export interface ZipButtonOptions { diff --git a/components/upload/nz-upload-list.component.html b/components/upload/nz-upload-list.component.html index 31532582e80..f7e62d09bbd 100644 --- a/components/upload/nz-upload-list.component.html +++ b/components/upload/nz-upload-list.component.html @@ -1,25 +1,25 @@
- - -
{{ locale.uploading }}
- -
+ +
{{ locale.uploading }}
- - + + - + - + + + - {{ file.name }} @@ -27,28 +27,26 @@
- - - - - - + + + + - - - + + +
- - - - - - + + + + + + diff --git a/components/upload/nz-upload-list.component.ts b/components/upload/nz-upload-list.component.ts index a6ce89149c5..f7a29719ec2 100644 --- a/components/upload/nz-upload-list.component.ts +++ b/components/upload/nz-upload-list.component.ts @@ -1,5 +1,5 @@ import { animate, style, transition, trigger } from '@angular/animations'; -import { Component, ElementRef, Input, OnChanges, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, ViewEncapsulation } from '@angular/core'; import { NzUpdateHostClassService } from '../core/services/update-host-class.service'; @@ -21,15 +21,32 @@ import { ShowUploadListInterface, UploadFile, UploadListType } from './interface ]) ], preserveWhitespaces: false, - encapsulation : ViewEncapsulation.None + encapsulation : ViewEncapsulation.None, + changeDetection : ChangeDetectionStrategy.OnPush }) export class NzUploadListComponent implements OnChanges { + private imageTypes = ['image', 'webp', 'png', 'svg', 'gif', 'jpg', 'jpeg', 'bmp']; + private _items: UploadFile[]; + + get showPic(): boolean { + return this.listType === 'picture' || this.listType === 'picture-card'; + } + // #region fields // tslint:disable-next-line:no-any @Input() locale: any = {}; @Input() listType: UploadListType; - @Input() items: UploadFile[]; + @Input() + set items(list: UploadFile[]) { + list.forEach(file => { + file.linkProps = typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps; + }); + this._items = list; + } + get items(): UploadFile[] { + return this._items; + } @Input() icons: ShowUploadListInterface; @Input() onPreview: (file: UploadFile) => void; @Input() onRemove: (file: UploadFile) => void; @@ -52,6 +69,75 @@ export class NzUploadListComponent implements OnChanges { // #region render + private extname(url: string): string { + const temp = url.split('/'); + const filename = temp[temp.length - 1]; + const filenameWithoutSuffix = filename.split(/#|\?/)[0]; + return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0]; + } + + isImageUrl(file: UploadFile): boolean { + if (~this.imageTypes.indexOf(file.type)) { + return true; + } + const url: string = (file.thumbUrl || file.url || '') as string; + if (!url) { + return false; + } + const extension = this.extname(url); + if (/^data:image\//.test(url) || /(webp|svg|png|gif|jpg|jpeg|bmp)$/i.test(extension)) { + return true; + } else if (/^data:/.test(url)) { + // other file types of base64 + return false; + } else if (extension) { + // other file types which have extension + return false; + } + return true; + } + + private previewFile(file: File | Blob, callback: (dataUrl: string) => void): void { + if (file.type && this.imageTypes.indexOf(file.type) === -1) { + callback(''); + } + const reader = new FileReader(); + // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL + reader.onloadend = () => callback(reader.result as string); + reader.readAsDataURL(file); + } + + private genThumb(): void { + // tslint:disable-next-line:no-any + const win = window as any; + if ( + !this.showPic || + typeof document === 'undefined' || + typeof win === 'undefined' || + !win.FileReader || + !win.File + ) { + return ; + } + this.items + .filter(file => file.originFileObj instanceof File && file.thumbUrl === undefined) + .forEach(file => { + file.thumbUrl = ''; + this.previewFile(file.originFileObj, (previewDataUrl: string) => { + file.thumbUrl = previewDataUrl; + this.detectChanges(); + }); + }); + } + + showPreview(file: UploadFile): boolean { + const { showPreviewIcon, hidePreviewIconInNonImage } = this.icons; + if (!showPreviewIcon) { + return false; + } + return this.isImageUrl(file) ? true : !hidePreviewIconInNonImage; + } + handlePreview(file: UploadFile, e: Event): void { if (!this.onPreview) { return; @@ -71,10 +157,15 @@ export class NzUploadListComponent implements OnChanges { // #endregion - constructor(private el: ElementRef, private updateHostClassService: NzUpdateHostClassService) { + constructor(private el: ElementRef, private cdr: ChangeDetectorRef, private updateHostClassService: NzUpdateHostClassService) { + } + + detectChanges(): void { + this.cdr.detectChanges(); } ngOnChanges(): void { this.setClassMap(); + this.genThumb(); } } diff --git a/components/upload/nz-upload.component.html b/components/upload/nz-upload.component.html index 4cd489b5290..83d13079e41 100644 --- a/components/upload/nz-upload.component.html +++ b/components/upload/nz-upload.component.html @@ -1,8 +1,8 @@ - @@ -10,7 +10,7 @@
-
+
diff --git a/components/upload/nz-upload.component.ts b/components/upload/nz-upload.component.ts index 568457d7618..b3a6ed4e288 100644 --- a/components/upload/nz-upload.component.ts +++ b/components/upload/nz-upload.component.ts @@ -30,6 +30,7 @@ import { ZipButtonOptions } from './interface'; import { NzUploadBtnComponent } from './nz-upload-btn.component'; +import { NzUploadListComponent } from './nz-upload-list.component'; @Component({ selector : 'nz-upload', @@ -40,7 +41,8 @@ import { NzUploadBtnComponent } from './nz-upload-btn.component'; }) export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { private i18n$: Subscription; - @ViewChild('upload') upload: NzUploadBtnComponent; + @ViewChild('uploadComp') uploadComp: NzUploadBtnComponent; + @ViewChild('listComp') listComp: NzUploadListComponent; // tslint:disable-next-line:no-any locale: any = {}; @@ -112,7 +114,8 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { if (typeof this.nzShowUploadList === 'boolean' && this.nzShowUploadList) { this.nzShowUploadList = { showPreviewIcon: true, - showRemoveIcon : true + showRemoveIcon : true, + hidePreviewIconInNonImage: false }; } // filters @@ -195,39 +198,16 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { (file.error && file.error.statusText) || this.locale.uploadError; } - private genThumb(file: UploadFile): void { - // tslint:disable-next-line:no-any - const win = window as any; - if ( - (this.nzListType !== 'picture' && this.nzListType !== 'picture-card') || - typeof document === 'undefined' || - typeof win === 'undefined' || - !win.FileReader || - !win.File || - !(file.originFileObj instanceof File) || - file.thumbUrl != null - ) { - return; - } - - file.thumbUrl = ''; - - const reader = new FileReader(); - reader.onloadend = () => file.thumbUrl = reader.result as string; - reader.readAsDataURL(file.originFileObj); - } - private onStart = (file: UploadFile): void => { if (!this.nzFileList) { this.nzFileList = []; } const targetItem = this.fileToObject(file); targetItem.status = 'uploading'; - this.nzFileList.push(targetItem); - this.genThumb(targetItem); + this.nzFileList = this.nzFileList.concat(targetItem); this.nzFileListChange.emit(this.nzFileList); this.nzChange.emit({ file: targetItem, fileList: this.nzFileList, type: 'start' }); - this.cdr.markForCheck(); + this.detectChangesList(); } private onProgress = (e: { percent: number }, file: UploadFile): void => { @@ -240,7 +220,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { fileList: this.nzFileList, type : 'progress' }); - this.cdr.detectChanges(); + this.detectChangesList(); } private onSuccess = (res: {}, file: UploadFile): void => { @@ -253,7 +233,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { fileList, type: 'success' }); - this.cdr.detectChanges(); + this.detectChangesList(); } private onError = (err: {}, file: UploadFile): void => { @@ -267,7 +247,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { fileList, type: 'error' }); - this.cdr.detectChanges(); + this.detectChangesList(); } // #endregion @@ -288,8 +268,13 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { // #region list + private detectChangesList(): void { + this.cdr.detectChanges(); + this.listComp.detectChanges(); + } + onRemove = (file: UploadFile): void => { - this.upload.abort(file); + this.uploadComp.abort(file); file.status = 'removed'; const fnRes = typeof this.nzRemove === 'function' ? this.nzRemove(file) : this.nzRemove == null ? true : this.nzRemove; @@ -342,7 +327,7 @@ export class NzUploadComponent implements OnInit, OnChanges, OnDestroy { ngOnInit(): void { this.i18n$ = this.i18n.localeChange.subscribe(() => { this.locale = this.i18n.getLocaleData('Upload'); - this.cdr.detectChanges(); + this.detectChangesList(); }); } diff --git a/components/upload/upload.spec.ts b/components/upload/upload.spec.ts index 03c33689531..56125412d75 100644 --- a/components/upload/upload.spec.ts +++ b/components/upload/upload.spec.ts @@ -293,6 +293,13 @@ describe('upload', () => { expect(instance._beforeUpload).toBe(false); }); describe('using observable', () => { + it('can return true', () => { + spyOn(instance, 'nzChange'); + instance.beforeUpload = (file: UploadFile, fileList: UploadFile[]): Observable => of(true); + fixture.detectChanges(); + pageObject.postSmall(); + expect(instance.nzChange).toHaveBeenCalled(); + }); it('can return same file', () => { let ret = false; instance.beforeUpload = (file: UploadFile, fileList: UploadFile[]): Observable => { @@ -565,7 +572,7 @@ describe('upload', () => { describe('[test boundary]', () => { it('clean a not exists request', () => { - instance.comp.upload.reqs.test = null; + instance.comp.uploadComp.reqs.test = null; instance.show = false; fixture.detectChanges(); expect(true).toBe(true); @@ -684,6 +691,36 @@ describe('upload', () => { expect(actions.length).toBe(0); expect(instance._onRemove).toBe(false); }); + it('should be hide preview when is invalid image url', fakeAsync(() => { + instance.icons = { + showPreviewIcon: true, + showRemoveIcon: true, + hidePreviewIconInNonImage: false + }; + instance.items = [ + { url: '1.pdf' } + ]; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const actions = dl.queryAll(By.css('.ant-upload-list-item-actions a')); + expect(actions.length).toBe(1); + })); + it('should be hide preview when is invalid image url', fakeAsync(() => { + instance.icons = { + showPreviewIcon: true, + showRemoveIcon: true, + hidePreviewIconInNonImage: true + }; + instance.items = [ + { url: '1.pdf' } + ]; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const actions = dl.queryAll(By.css('.ant-upload-list-item-actions a')); + expect(actions.length).toBe(0); + })); }); describe('[onPreview]', () => { @@ -699,6 +736,41 @@ describe('upload', () => { dl.query(By.css('.ant-upload-list-item-actions a')).nativeElement.click(); expect(instance._onPreview).toBe(false); }); + it('should support linkProps as object', fakeAsync(() => { + instance.items = [ + { + uid: '-1', + name: 'foo.png', + status: 'done', + url: 'http://www.baidu.com/xxx.png', + linkProps: { + download: 'image' + } + } + ]; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const el = dl.query(By.css('.ant-upload-list-item-name')).nativeElement as HTMLElement; + expect(el.attributes.getNamedItem('download').textContent).toBe('image'); + })); + it('should support linkProps as json stringify', fakeAsync(() => { + const linkPropsString = JSON.stringify({ download: 'image' }); + instance.items = [ + { + uid: '-1', + name: 'foo.png', + status: 'done', + url: 'http://www.baidu.com/xxx.png', + linkProps: linkPropsString + } + ]; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const el = dl.query(By.css('.ant-upload-list-item-name')).nativeElement as HTMLElement; + expect(el.attributes.getNamedItem('download').textContent).toBe('image'); + })); }); describe('[onRemove]', () => { @@ -715,6 +787,61 @@ describe('upload', () => { expect(instance._onRemove).toBe(false); }); }); + + describe('[isImageUrl]', () => { + describe('via image type', () => { + it('should be true when file object type value is a valid image', () => { + expect(instance.comp.isImageUrl({ type: 'image' } as any)).toBe(true); + }); + }); + describe('via thumbUrl or url', () => { + it('should be false when not found url & thumbUrl', () => { + expect(instance.comp.isImageUrl({ } as any)).toBe(false); + }); + describe('via extension', () => { + it('with valid image extension', () => { + expect(instance.comp.isImageUrl({ url: '1.svg' } as any)).toBe(true); + }); + it('with invalid image extension', () => { + expect(instance.comp.isImageUrl({ url: '1.pdf' } as any)).toBe(false); + }); + }); + describe('when url is base64', () => { + it('with valid image base64', () => { + expect(instance.comp.isImageUrl({ url: 'data:image/png;base64,1' } as any)).toBe(true); + }); + it('with invalid image base64', () => { + expect(instance.comp.isImageUrl({ url: 'data:application/pdf;base64,1' } as any)).toBe(false); + }); + }); + }); + }); + + describe('[genThumb]', () => { + class MockFR { + result = '1'; + onloadend(dataUrl: string): void { } + readAsDataURL(): void { + this.onloadend('1'); + } + } + it('should be generate thumb when is valid image data', () => { + spyOn(window as any, 'FileReader').and.returnValue(new MockFR()); + instance.listType = 'picture'; + instance.items = [ { originFileObj: new File([''], '1.png', { type: 'image' }), thumbUrl: undefined } ]; + fixture.detectChanges(); + expect(instance.items[0].thumbUrl).toBe('1'); + }); + it('should be ingore thumb when is invalid image data', () => { + const mockFR = new MockFR(); + mockFR.result = ''; + spyOn(window as any, 'FileReader').and.returnValue(mockFR); + instance.listType = 'picture'; + instance.items = [ { originFileObj: new File([''], '1.pdf', { type: 'pdf' }), thumbUrl: undefined } ]; + fixture.detectChanges(); + expect(instance.items[0].thumbUrl).toBe(''); + }); + }); }); describe('btn', () => { @@ -1140,7 +1267,6 @@ class TestUploadComponent { [icons]="icons" [onPreview]="onPreview" [onRemove]="onRemove">`, - styleUrls: [ './style/index.less' ], encapsulation: ViewEncapsulation.None }) class TestUploadListComponent {