Skip to content

Commit

Permalink
fix(module:upload): previewing images should be cancellable when the …
Browse files Browse the repository at this point in the history
…component gets destroyed (#7067)
  • Loading branch information
arturovt committed Nov 22, 2021
1 parent 7fac8eb commit 8f21ef1
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 52 deletions.
112 changes: 64 additions & 48 deletions components/upload/upload-list.component.ts
Expand Up @@ -16,9 +16,11 @@ import {
Input,
NgZone,
OnChanges,
OnDestroy,
ViewEncapsulation
} from '@angular/core';
import { Observable } from 'rxjs';
import { fromEvent, Observable, of, Subject } from 'rxjs';
import { takeUntil, map } from 'rxjs/operators';

import { NzSafeAny } from 'ng-zorro-antd/core/types';

Expand Down Expand Up @@ -60,7 +62,7 @@ interface UploadListFile extends NzUploadFile {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NzUploadListComponent implements OnChanges {
export class NzUploadListComponent implements OnChanges, OnDestroy {
list: UploadListFile[] = [];

private get showPic(): boolean {
Expand All @@ -82,6 +84,8 @@ export class NzUploadListComponent implements OnChanges {
@Input() iconRender: NzIconRenderTemplate | null = null;
@Input() dir: Direction = 'ltr';

private destroy$ = new Subject<void>();

private genErr(file: NzUploadFile): string {
if (file.response && typeof file.response === 'string') {
return file.response;
Expand Down Expand Up @@ -128,47 +132,47 @@ export class NzUploadListComponent implements OnChanges {
}
}

private previewImage(file: File | Blob): Promise<string> {
return new Promise(resolve => {
if (!isImageFileType(file.type) || !this.platform.isBrowser) {
resolve('');
return;
}
this.ngZone.runOutsideAngular(() => {
const canvas = this.doc.createElement('canvas');
canvas.width = MEASURE_SIZE;
canvas.height = MEASURE_SIZE;
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
this.doc.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
const { width, height } = img;

let drawWidth = MEASURE_SIZE;
let drawHeight = MEASURE_SIZE;
let offsetX = 0;
let offsetY = 0;

if (width < height) {
drawHeight = height * (MEASURE_SIZE / width);
offsetY = -(drawHeight - drawWidth) / 2;
} else {
drawWidth = width * (MEASURE_SIZE / height);
offsetX = -(drawWidth - drawHeight) / 2;
}

try {
ctx!.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
} catch {}
const dataURL = canvas.toDataURL();
this.doc.body.removeChild(canvas);

resolve(dataURL);
};
img.src = window.URL.createObjectURL(file);
});
});
private previewImage(file: File | Blob): Observable<string> {
if (!isImageFileType(file.type) || !this.platform.isBrowser) {
return of('');
}

const canvas = this.doc.createElement('canvas');
canvas.width = MEASURE_SIZE;
canvas.height = MEASURE_SIZE;
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
this.doc.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.src = objectUrl;
return fromEvent(img, 'load').pipe(
map(() => {
const { width, height } = img;

let drawWidth = MEASURE_SIZE;
let drawHeight = MEASURE_SIZE;
let offsetX = 0;
let offsetY = 0;

if (width < height) {
drawHeight = height * (MEASURE_SIZE / width);
offsetY = -(drawHeight - drawWidth) / 2;
} else {
drawWidth = width * (MEASURE_SIZE / height);
offsetX = -(drawWidth - drawHeight) / 2;
}

try {
ctx!.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
} catch {}
const dataURL = canvas.toDataURL();
this.doc.body.removeChild(canvas);

URL.revokeObjectURL(objectUrl);
return dataURL;
})
);
}

private genThumb(): void {
Expand All @@ -190,12 +194,20 @@ export class NzUploadListComponent implements OnChanges {
.filter(file => file.originFileObj instanceof File && file.thumbUrl === undefined)
.forEach(file => {
file.thumbUrl = '';
(this.previewFile ? this.previewFile(file).toPromise() : this.previewImage(file.originFileObj!)).then(
dataUrl => {
file.thumbUrl = dataUrl;
this.detectChanges();
}
// Caretaker note: we shouldn't use promises here since they're not cancellable.
// A promise microtask can be resolved after the view is destroyed. Thus running `detectChanges()`
// will cause a runtime exception (`detectChanges()` cannot be run on destroyed views).
const dataUrl$ = (this.previewFile ? this.previewFile(file) : this.previewImage(file.originFileObj!)).pipe(
takeUntil(this.destroy$)
);
this.ngZone.runOutsideAngular(() => {
dataUrl$.subscribe(dataUrl => {
this.ngZone.run(() => {
file.thumbUrl = dataUrl;
this.detectChanges();
});
});
});
});
}

Expand Down Expand Up @@ -261,4 +273,8 @@ export class NzUploadListComponent implements OnChanges {
this.fixData();
this.genThumb();
}

ngOnDestroy(): void {
this.destroy$.next();
}
}
9 changes: 5 additions & 4 deletions components/upload/upload.spec.ts
Expand Up @@ -948,11 +948,12 @@ describe('upload', () => {
width = 1;
height = 2;

onload(): void {}

set src(_: string) {
this.onload();
addEventListener(_name: string, callback: VoidFunction): void {
callback();
}
removeEventListener(): void {}

set src(_: string) {}
}
it('should be generate thumb when is valid image data', fakeAsync(() => {
spyOn(window as any, 'Image').and.returnValue(new MockImage());
Expand Down

0 comments on commit 8f21ef1

Please sign in to comment.