diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index 166444112..c359e1c5b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -2191,9 +2191,6 @@ describe('DocumentDetailComponent', () => { const appendChildSpy = jest .spyOn(document.body, 'appendChild') .mockImplementation((node: Node) => node) - const removeChildSpy = jest - .spyOn(document.body, 'removeChild') - .mockImplementation((node: Node) => node) const createObjectURLSpy = jest .spyOn(URL, 'createObjectURL') .mockReturnValue('blob:mock-url') @@ -2212,6 +2209,7 @@ describe('DocumentDetailComponent', () => { src: '', onload: null, contentWindow: mockContentWindow, + remove: jest.fn(), } const createElementSpy = jest @@ -2255,12 +2253,11 @@ describe('DocumentDetailComponent', () => { mockContentWindow.onafterprint(new Event('afterprint')) } - expect(removeChildSpy).toHaveBeenCalledWith(mockIframe) + expect(mockIframe.remove).toHaveBeenCalled() expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') createElementSpy.mockRestore() appendChildSpy.mockRestore() - removeChildSpy.mockRestore() createObjectURLSpy.mockRestore() revokeObjectURLSpy.mockRestore() }) @@ -2307,9 +2304,6 @@ describe('DocumentDetailComponent', () => { const appendChildSpy = jest .spyOn(document.body, 'appendChild') .mockImplementation((node: Node) => node) - const removeChildSpy = jest - .spyOn(document.body, 'removeChild') - .mockImplementation((node: Node) => node) const createObjectURLSpy = jest .spyOn(URL, 'createObjectURL') .mockReturnValue('blob:mock-url') @@ -2332,6 +2326,7 @@ describe('DocumentDetailComponent', () => { src: '', onload: null, contentWindow: mockContentWindow, + remove: jest.fn(), } const createElementSpy = jest @@ -2354,15 +2349,21 @@ describe('DocumentDetailComponent', () => { if (expectToast) { expect(toastSpy).toHaveBeenCalled() + expect(mockIframe.remove).toHaveBeenCalled() + expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') } else { expect(toastSpy).not.toHaveBeenCalled() + expect(mockIframe.remove).not.toHaveBeenCalled() + expect(revokeObjectURLSpy).not.toHaveBeenCalled() + + component.ngOnDestroy() + + expect(mockIframe.remove).toHaveBeenCalled() + expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') } - expect(removeChildSpy).toHaveBeenCalledWith(mockIframe) - expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') createElementSpy.mockRestore() appendChildSpy.mockRestore() - removeChildSpy.mockRestore() createObjectURLSpy.mockRestore() revokeObjectURLSpy.mockRestore() }) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 3820f9ed1..0620733a4 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -289,6 +289,8 @@ export class DocumentDetailComponent private incomingUpdateModal: NgbModalRef private pendingIncomingUpdate: IncomingDocumentUpdate private lastLocalSaveModified: string | null = null + private printIframe: HTMLIFrameElement | null = null + private printBlobUrl: string | null = null requiresPassword: boolean = false password: string @@ -868,6 +870,7 @@ export class DocumentDetailComponent } ngOnDestroy(): void { + this.cleanupPrintDocument() this.unsubscribeNotifier.next(this) this.unsubscribeNotifier.complete() } @@ -1889,6 +1892,7 @@ export class DocumentDetailComponent } printDocument() { + this.cleanupPrintDocument() const selectedVersionId = this.getSelectedNonLatestVersionId() const printUrl = this.documentsService.getDownloadUrl( this.document().id, @@ -1902,6 +1906,8 @@ export class DocumentDetailComponent next: (blob) => { const blobUrl = URL.createObjectURL(blob) const iframe = document.createElement('iframe') + this.printIframe = iframe + this.printBlobUrl = blobUrl iframe.style.position = 'fixed' iframe.style.right = '0' iframe.style.bottom = '0' @@ -1917,22 +1923,19 @@ export class DocumentDetailComponent iframe.contentWindow.focus() iframe.contentWindow.print() iframe.contentWindow.onafterprint = () => { - document.body.removeChild(iframe) - URL.revokeObjectURL(blobUrl) + this.cleanupPrintDocument() } } catch (err) { // FF throws cross-origin error on onafterprint const isCrossOriginAfterPrintError = err instanceof DOMException && err.message.includes('onafterprint') + // FF throws here while print preview is still reading the iframe + // so keep it alive until the next print or teardown if (!isCrossOriginAfterPrintError) { this.toastService.showError($localize`Print failed.`, err) + timer(100).subscribe(() => this.cleanupPrintDocument()) } - timer(100).subscribe(() => { - // delay to avoid FF print failure - document.body.removeChild(iframe) - URL.revokeObjectURL(blobUrl) - }) } }) } @@ -1945,6 +1948,15 @@ export class DocumentDetailComponent }) } + private cleanupPrintDocument() { + if (this.printIframe) this.printIframe.remove() + this.printIframe = null + if (this.printBlobUrl) { + URL.revokeObjectURL(this.printBlobUrl) + this.printBlobUrl = null + } + } + public openShareLinks() { const modal = this.modalService.open(ShareLinksDialogComponent) modal.componentInstance.documentId.set(this.document().id)