AAE-45391 Lazy loading and improvements for pdfJS (#11869)

* [AAE-45391] - Lazy loading some of the big external libs

* [AAE-45391] - Added some extra units to pdfjs viewer

* [AAE-45391] - Some improement on viewer component

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - fixed schematic

* [AAE-45391] - Added some docs for the schematics as well

* [AAE-37328] - Fixed comment on missing provider
This commit is contained in:
Vito Albano
2026-05-11 14:17:39 +01:00
committed by GitHub
parent 62fb51e910
commit 9fe4006a0d
42 changed files with 939 additions and 65 deletions
@@ -1,31 +0,0 @@
<div mat-dialog-title>
<mat-icon adf-icon="lock" />
</div>
<mat-dialog-content>
<form (submit)="submit()">
<mat-form-field class="adf-full-width">
<input matInput
data-automation-id='adf-password-dialog-input'
type="password"
placeholder="{{ 'ADF_VIEWER.PDF_DIALOG.PLACEHOLDER' | translate }}"
[formControl]="passwordFormControl" />
</mat-form-field>
<mat-error *ngIf="isError()" data-automation-id='adf-password-dialog-error'>{{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}</mat-error>
</form>
</mat-dialog-content>
<mat-dialog-actions class="adf-dialog-buttons">
<span class="adf-fill-remaining-space"></span>
<button mat-button mat-dialog-close data-automation-id='adf-password-dialog-close'>{{ 'ADF_VIEWER.PDF_DIALOG.CLOSE' | translate }}</button>
<button mat-button
data-automation-id='adf-password-dialog-submit'
class="adf-dialog-action-button"
[disabled]="!isValid()"
(click)="submit()">
{{ 'ADF_VIEWER.PDF_DIALOG.SUBMIT' | translate }}
</button>
</mat-dialog-actions>
@@ -1,11 +0,0 @@
.adf-fill-remaining-space {
flex: 1 1 auto;
}
.adf-full-width {
width: 100%;
}
.adf-dialog-buttons button {
text-transform: uppercase;
}
@@ -1,102 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PdfPasswordDialogComponent } from './pdf-viewer-password-dialog';
declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_PASSWORD: number } };
describe('PdfPasswordDialogComponent', () => {
let component: PdfPasswordDialogComponent;
let fixture: ComponentFixture<PdfPasswordDialogComponent>;
let dialogRef: MatDialogRef<PdfPasswordDialogComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [PdfPasswordDialogComponent],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: {
reason: null
}
},
{
provide: MatDialogRef,
useValue: {
close: jasmine.createSpy('open')
}
}
]
});
fixture = TestBed.createComponent(PdfPasswordDialogComponent);
component = fixture.componentInstance;
dialogRef = TestBed.inject(MatDialogRef);
});
it('should have empty default value', () => {
fixture.detectChanges();
expect(component.passwordFormControl.value).toBe('');
});
describe('isError', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should return false', () => {
component.data.reason = pdfjsLib.PasswordResponses.NEED_PASSWORD;
expect(component.isError()).toBe(false);
});
it('should return true', () => {
component.data.reason = pdfjsLib.PasswordResponses.INCORRECT_PASSWORD;
expect(component.isError()).toBe(true);
});
});
describe('isValid', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should return false when input has no value', () => {
component.passwordFormControl.setValue('');
expect(component.isValid()).toBe(false);
});
it('should return true when input has a valid value', () => {
component.passwordFormControl.setValue('some-text');
expect(component.isValid()).toBe(true);
});
});
it('should close dialog with input value', () => {
fixture.detectChanges();
component.passwordFormControl.setValue('some-value');
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith('some-value');
});
});
@@ -1,58 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgIf } from '@angular/common';
import { Component, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { TranslatePipe } from '@ngx-translate/core';
import { IconModule } from '../../../icon/icon.module';
declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_PASSWORD: number } };
@Component({
selector: 'adf-pdf-viewer-password-dialog',
templateUrl: './pdf-viewer-password-dialog.html',
styleUrls: ['./pdf-viewer-password-dialog.scss'],
imports: [MatDialogModule, IconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslatePipe, NgIf, MatButtonModule],
encapsulation: ViewEncapsulation.None
})
export class PdfPasswordDialogComponent implements OnInit {
private readonly dialogRef = inject<MatDialogRef<PdfPasswordDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
passwordFormControl: UntypedFormControl;
ngOnInit() {
this.passwordFormControl = new UntypedFormControl('', [Validators.required]);
}
isError(): boolean {
return this.data.reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD;
}
isValid(): boolean {
return !this.passwordFormControl.hasError('required');
}
submit(): void {
this.dialogRef.close(this.passwordFormControl.value);
}
}
@@ -1,6 +0,0 @@
<ng-container *ngIf="image$ | async as image">
<img [src]="image" role="button"
[alt]="'ADF_VIEWER.SIDEBAR.THUMBNAILS.PAGE' | translate: { pageNum: page.id }"
title="{{ 'ADF_VIEWER.SIDEBAR.THUMBNAILS.PAGE' | translate: { pageNum: page.id } }}"
[attr.aria-label]="'ADF_VIEWER.SIDEBAR.THUMBNAILS.PAGE' | translate: { pageNum: page.id }">
</ng-container>
@@ -1,73 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DomSanitizer } from '@angular/platform-browser';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PdfThumbComponent } from './pdf-viewer-thumb.component';
describe('PdfThumbComponent', () => {
let fixture: ComponentFixture<PdfThumbComponent>;
let component: PdfThumbComponent;
const domSanitizer = {
bypassSecurityTrustUrl: () => 'image-data'
};
const width = 91;
const height = 119;
const page = {
id: 1,
getPage: jasmine.createSpy('getPage').and.returnValue(
Promise.resolve({
getViewport: () => ({ width, height }),
render: jasmine.createSpy('render').and.returnValue({ promise: Promise.resolve() })
})
),
getWidth: jasmine.createSpy('getWidth').and.returnValue(width),
getHeight: jasmine.createSpy('getHeight').and.returnValue(height)
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [PdfThumbComponent],
providers: [
{
provide: DomSanitizer,
useValue: domSanitizer
}
]
});
fixture = TestBed.createComponent(PdfThumbComponent);
component = fixture.componentInstance;
});
it('should have resolve image data', (done) => {
component.page = page;
fixture.detectChanges();
component.image$.then((result) => {
expect(result).toBe('image-data');
done();
});
});
it('should focus element', () => {
component.page = page;
fixture.detectChanges();
component.focus();
expect(fixture.debugElement.nativeElement.id).toBe(document.activeElement.id);
});
});
@@ -1,71 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FocusableOption } from '@angular/cdk/a11y';
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, ElementRef, Input, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { TranslatePipe } from '@ngx-translate/core';
@Component({
selector: 'adf-pdf-thumb',
templateUrl: './pdf-viewer-thumb.component.html',
encapsulation: ViewEncapsulation.None,
imports: [AsyncPipe, TranslatePipe, NgIf],
host: { tabindex: '0' }
})
export class PdfThumbComponent implements OnInit, FocusableOption {
private readonly sanitizer = inject(DomSanitizer);
private readonly element = inject(ElementRef);
@Input()
page: any;
image$: Promise<string>;
ngOnInit() {
this.image$ = this.page.getPage().then((page) => this.getThumb(page));
}
focus() {
this.element.nativeElement.focus();
}
private getThumb(page): Promise<string> {
const viewport = page.getViewport({ scale: 1 });
const canvas = this.getCanvas();
const scale = Math.min(canvas.height / viewport.height, canvas.width / viewport.width);
return page
.render({
canvasContext: canvas.getContext('2d'),
viewport: page.getViewport({ scale })
})
.promise.then(() => {
const imageSource = canvas.toDataURL();
return this.sanitizer.bypassSecurityTrustUrl(imageSource);
});
}
private getCanvas(): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = this.page.getWidth();
canvas.height = this.page.getHeight();
return canvas;
}
}
@@ -1,13 +0,0 @@
<div class="adf-pdf-thumbnails__content"
data-automation-id='adf-thumbnails-content'
[style.height.px]="virtualHeight"
[style.transform]="'translate(-50%, ' + translateY + 'px)'">
@for (page of renderItems; track page.id) {
<adf-pdf-thumb
class="adf-pdf-thumbnails__thumb"
[id]="page.id"
[ngClass]="{'adf-pdf-thumbnails__thumb--selected' : isSelected(page.id)}"
[page]="page"
(click)="goTo(page.id)" />
}
</div>
@@ -1,30 +0,0 @@
.adf-pdf-thumbnails {
display: block;
overflow: hidden;
overflow-y: auto;
height: 100%;
position: relative;
&__content {
top: 5px;
left: 50%;
height: 0;
position: absolute;
}
&__thumb {
cursor: pointer;
display: block;
width: 91px;
background: var(--mat-sys-surface);
margin-bottom: 15px;
}
&__thumb:hover {
box-shadow: var(--mat-sys-level2);
}
&__thumb--selected {
border: 2px solid var(--mat-sys-primary);
}
}
@@ -1,236 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PdfThumbListComponent } from './pdf-viewer-thumbnails.component';
import { UnitTestingUtils } from '../../../testing';
import { DOWN_ARROW, ESCAPE, UP_ARROW } from '@angular/cdk/keycodes';
declare const pdfjsViewer: any;
describe('PdfThumbListComponent', () => {
let fixture: ComponentFixture<PdfThumbListComponent>;
let component: PdfThumbListComponent;
let testingUtils: UnitTestingUtils;
const page = (id) => ({
id,
getPage: Promise.resolve()
});
const viewerMock = {
_currentPageNumber: null,
set currentPageNumber(pageNum) {
this._currentPageNumber = pageNum;
/* cspell:disable-next-line */
this.eventBus.dispatch('pagechanging', { pageNumber: pageNum });
},
get currentPageNumber() {
return this._currentPageNumber;
},
pdfDocument: {
getPage: () =>
Promise.resolve({
getViewport: () => ({ height: 421, width: 335 }),
render: jasmine.createSpy('render').and.returnValue({ promise: Promise.resolve() })
})
},
_pages: [
page(1),
page(2),
page(3),
page(4),
page(5),
page(6),
page(7),
page(8),
page(9),
page(10),
page(11),
page(12),
page(13),
page(14),
page(15),
page(16)
],
eventBus: new pdfjsViewer.EventBus()
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [PdfThumbListComponent]
});
fixture = TestBed.createComponent(PdfThumbListComponent);
testingUtils = new UnitTestingUtils(fixture.debugElement);
component = fixture.componentInstance;
component.pdfViewer = viewerMock as any;
// provide scrollable container
fixture.nativeElement.style.display = 'block';
fixture.nativeElement.style.height = '700px';
fixture.nativeElement.style.overflow = 'scroll';
const content = testingUtils.getByCSS('.adf-pdf-thumbnails__content').nativeElement;
content.style.height = '2000px';
content.style.position = 'unset';
});
it('should render initial rage of items', () => {
fixture.nativeElement.scrollTop = 0;
fixture.detectChanges();
const renderedIds = component.renderItems.map((item) => item.id);
// eslint-disable-next-line no-underscore-dangle
const rangeIds = viewerMock._pages.slice(0, 6).map((item) => item.id);
expect(renderedIds).toEqual(rangeIds);
});
it('should render next range on scroll', () => {
component.currentHeight = 114;
fixture.nativeElement.scrollTop = 700;
fixture.detectChanges();
const renderedIds = component.renderItems.map((item) => item.id);
// eslint-disable-next-line no-underscore-dangle
const rangeIds = viewerMock._pages.slice(5, 12).map((item) => item.id);
expect(renderedIds).toEqual(rangeIds);
});
it('should render items containing current document page', () => {
fixture.detectChanges();
const renderedIds = component.renderItems.map((item) => item.id);
expect(renderedIds).not.toContain(10);
component.scrollInto(10);
const newRenderedIds = component.renderItems.map((item) => item.id);
expect(newRenderedIds).toContain(10);
});
it('should not change items if range contains current document page', () => {
fixture.nativeElement.scrollTop = 1700;
fixture.detectChanges();
const renderedIds = component.renderItems.map((item) => item.id);
expect(renderedIds).toContain(12);
/* cspell:disable-next-line */
viewerMock.eventBus.dispatch('pagechanging', { pageNumber: 12 });
const newRenderedIds = component.renderItems.map((item) => item.id);
expect(newRenderedIds).toContain(12);
});
it('should scroll thumbnail height amount to buffer thumbnail onPageChange event', () => {
spyOn(component, 'scrollInto');
fixture.detectChanges();
expect(component.renderItems[component.renderItems.length - 1].id).toBe(6);
expect(fixture.debugElement.nativeElement.scrollTop).toBe(0);
component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 6 });
expect(component.scrollInto).not.toHaveBeenCalled();
expect(fixture.debugElement.nativeElement.scrollTop).toBe(0);
});
it('should set active current page on onPageChange event', () => {
fixture.detectChanges();
component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 6 });
expect(document.activeElement.id).toBe('6');
});
it('should return current viewed page as selected', () => {
fixture.nativeElement.scrollTop = 0;
fixture.detectChanges();
viewerMock.currentPageNumber = 2;
expect(component.isSelected(2)).toBe(true);
});
it('should go to selected page', () => {
fixture.detectChanges();
component.goTo(12);
expect(viewerMock.currentPageNumber).toBe(12);
});
describe('Keyboard events', () => {
it('should select next page in the list on DOWN_ARROW event', () => {
const event = new KeyboardEvent('keydown', { keyCode: DOWN_ARROW } as KeyboardEventInit);
fixture.detectChanges();
component.goTo(1);
expect(document.activeElement.id).toBe('1');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.id).toBe('2');
});
it('should select previous page in the list on UP_ARROW event', () => {
const event = new KeyboardEvent('keydown', { keyCode: UP_ARROW } as KeyboardEventInit);
fixture.detectChanges();
component.goTo(2);
expect(document.activeElement.id).toBe('2');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.id).toBe('1');
});
it('should not select previous page if it is the first page', () => {
const event = new KeyboardEvent('keydown', { keyCode: UP_ARROW } as KeyboardEventInit);
fixture.detectChanges();
component.goTo(1);
expect(document.activeElement.id).toBe('1');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.id).toBe('1');
});
it('should not select next item if it is the last page', () => {
const event = new KeyboardEvent('keydown', { keyCode: DOWN_ARROW } as KeyboardEventInit);
fixture.detectChanges();
component.scrollInto(16);
fixture.detectChanges();
component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 16 });
expect(document.activeElement.id).toBe('16');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.id).toBe('16');
});
it('should emit on ESCAPE event', () => {
const event = new KeyboardEvent('keydown', { keyCode: ESCAPE } as KeyboardEventInit);
spyOn(component.close, 'emit');
fixture.detectChanges();
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(component.close.emit).toHaveBeenCalled();
});
});
});
@@ -1,246 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FocusKeyManager } from '@angular/cdk/a11y';
import { DOWN_ARROW, ESCAPE, TAB, UP_ARROW } from '@angular/cdk/keycodes';
import { DOCUMENT, NgClass } from '@angular/common';
import {
AfterViewInit,
Component,
ContentChild,
ElementRef,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
TemplateRef,
ViewChildren,
ViewEncapsulation,
inject
} from '@angular/core';
import { delay } from 'rxjs/operators';
import { PdfThumbComponent } from '../pdf-viewer-thumb/pdf-viewer-thumb.component';
@Component({
selector: 'adf-pdf-thumbnails',
templateUrl: './pdf-viewer-thumbnails.component.html',
styleUrls: ['./pdf-viewer-thumbnails.component.scss'],
host: { class: 'adf-pdf-thumbnails' },
imports: [PdfThumbComponent, NgClass],
encapsulation: ViewEncapsulation.None
})
export class PdfThumbListComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly element = inject(ElementRef);
private readonly document = inject(DOCUMENT);
@Input({ required: true }) pdfViewer: any;
@Output()
close = new EventEmitter<void>();
virtualHeight: number = 0;
translateY: number = 0;
renderItems: any[] = [];
width: number = 91;
currentHeight: number = 0;
private items: any[] = [];
private readonly margin: number = 15;
private itemHeight: number = 114 + this.margin;
private previouslyFocusedElement: HTMLElement | null = null;
private keyManager: FocusKeyManager<PdfThumbComponent>;
@ContentChild(TemplateRef)
template: TemplateRef<unknown>;
@ViewChildren(PdfThumbComponent)
thumbsList: QueryList<PdfThumbComponent>;
@HostListener('keydown', ['$event'])
onKeydown(event: KeyboardEvent): void {
const keyCode = event.keyCode;
if (keyCode === UP_ARROW && this.canSelectPreviousItem()) {
this.pdfViewer.currentPageNumber -= 1;
}
if (keyCode === DOWN_ARROW && this.canSelectNextItem()) {
this.pdfViewer.currentPageNumber += 1;
}
if (keyCode === TAB) {
if (this.canSelectNextItem()) {
this.pdfViewer.currentPageNumber += 1;
} else {
this.close.emit();
}
}
if (keyCode === ESCAPE) {
this.close.emit();
}
this.keyManager.setFocusOrigin('keyboard');
event.preventDefault();
}
@HostListener('window:resize')
onResize() {
this.calculateItems();
}
constructor() {
this.calculateItems = this.calculateItems.bind(this);
this.onPageChange = this.onPageChange.bind(this);
}
ngOnInit() {
/* cspell:disable-next-line */
this.pdfViewer.eventBus.on('pagechanging', this.onPageChange);
this.element.nativeElement.addEventListener('scroll', this.calculateItems, true);
this.setHeight(this.pdfViewer.currentPageNumber);
this.items = this.getPages();
this.calculateItems();
this.previouslyFocusedElement = this.document.activeElement as HTMLElement;
}
ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.thumbsList);
this.thumbsList.changes.pipe(delay(0)).subscribe(() => this.keyManager.setActiveItem(this.getPageIndex(this.pdfViewer.currentPageNumber)));
setTimeout(() => {
this.scrollInto(this.pdfViewer.currentPageNumber);
this.keyManager.setActiveItem(this.getPageIndex(this.pdfViewer.currentPageNumber));
}, 0);
}
ngOnDestroy() {
this.element.nativeElement.removeEventListener('scroll', this.calculateItems, true);
/* cspell:disable-next-line */
this.pdfViewer.eventBus.on('pagechanging', this.onPageChange);
if (this.previouslyFocusedElement) {
this.previouslyFocusedElement.focus();
this.previouslyFocusedElement = null;
}
}
isSelected(pageNumber: number) {
return this.pdfViewer.currentPageNumber === pageNumber;
}
goTo(pageNumber: number) {
this.pdfViewer.currentPageNumber = pageNumber;
}
scrollInto(pageNumber: number) {
if (this.items.length) {
const index: number = this.items.findIndex((element) => element.id === pageNumber);
if (index < 0 || index >= this.items.length) {
return;
}
this.element.nativeElement.scrollTop = index * this.itemHeight;
this.calculateItems();
}
}
getPages(): any[] {
// eslint-disable-next-line no-underscore-dangle
return this.pdfViewer._pages.map((page) => ({
id: page.id,
getWidth: () => this.width,
getHeight: () => this.currentHeight,
getPage: () => this.pdfViewer.pdfDocument.getPage(page.id)
}));
}
private setHeight(id: number): Promise<void> {
return this.pdfViewer.pdfDocument.getPage(id).then((page) => this.calculateHeight(page));
}
private calculateHeight(page) {
const viewport = page.getViewport({ scale: 1 });
const pageRatio = viewport.width / viewport.height;
const height = Math.floor(this.width / pageRatio);
this.currentHeight = height;
this.itemHeight = height + this.margin;
}
private calculateItems() {
const { element, viewPort, itemsInView } = this.getContainerSetup();
const indexByScrollTop = ((element.scrollTop / viewPort) * this.items.length) / itemsInView;
const start = Math.floor(indexByScrollTop);
const end = Math.ceil(indexByScrollTop) + itemsInView;
this.translateY = this.itemHeight * Math.ceil(start);
this.virtualHeight = this.itemHeight * this.items.length - this.translateY;
this.renderItems = this.items.slice(start, end);
}
private getContainerSetup() {
const element = this.element.nativeElement;
const elementRec = element.getBoundingClientRect();
const itemsInView = Math.ceil(elementRec.height / this.itemHeight);
const viewPort = (this.itemHeight * this.items.length) / itemsInView;
return {
element,
viewPort,
itemsInView
};
}
private onPageChange(event: any) {
const index = this.renderItems.findIndex((element) => element.id === event.pageNumber);
if (index < 0) {
this.scrollInto(event.pageNumber);
}
if (index >= this.renderItems.length - 1) {
this.element.nativeElement.scrollTop += this.itemHeight;
}
this.keyManager.setActiveItem(this.getPageIndex(event.pageNumber));
}
private getPageIndex(pageNumber: number): number {
const thumbsListArray = this.thumbsList.toArray();
return thumbsListArray.findIndex((el) => el.page.id === pageNumber);
}
private canSelectNextItem(): boolean {
return this.pdfViewer.currentPageNumber !== this.pdfViewer.pagesCount;
}
private canSelectPreviousItem(): boolean {
return this.pdfViewer.currentPageNumber !== 1;
}
}
@@ -1,236 +0,0 @@
/* stylelint-disable selector-class-pattern */
.adf-pdf-viewer {
.textLayer {
position: absolute;
inset: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
border: 1px solid gray;
& > div {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0 0;
}
.adf-highlight {
margin: -1px;
padding: 1px;
background-color: rgb(180, 0, 170);
border-radius: 4px;
&.adf-begin {
border-radius: 4px 0 0 4px;
}
&.adf-end {
border-radius: 0 4px 4px 0;
}
&.adf-middle {
border-radius: 0;
}
&.adf-selected {
background-color: rgb(0, 100, 0);
}
}
&::selection {
background: rgb(0, 0, 255);
}
.adf-endOfContent {
display: block;
position: absolute;
inset: 0;
top: 100%;
z-index: -1;
cursor: default;
user-select: none;
&.adf-active {
top: 0;
}
}
}
.adf-annotationLayer {
section {
position: absolute;
}
.adf-linkAnnotation {
& > a {
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* stylelint-disable */
background: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7') 0 0 repeat;
/* stylelint-enable */
&:hover {
opacity: 0.2;
background: #ff0;
box-shadow: 0 2px 10px #ff0;
}
}
}
.adf-textAnnotation {
img {
position: absolute;
cursor: pointer;
}
}
.adf-popupWrapper {
position: absolute;
width: 20em;
}
.adf-popup {
position: absolute;
z-index: 200;
max-width: 20em;
background-color: #ff9;
box-shadow: 0 2px 5px #333;
border-radius: 2px;
padding: 0.6em;
margin-left: 5px;
cursor: pointer;
word-wrap: break-word;
h1 {
font-size: 1em;
border-bottom: 1px solid #000;
padding-bottom: 0.2em;
}
p {
padding-top: 0.2em;
}
}
.adf-highlightAnnotation,
.adf-underlineAnnotation,
.adf-squigglyAnnotation,
.adf-strikeoutAnnotation,
.adf-fileAttachmentAnnotation {
cursor: pointer;
}
}
.adf-pdfViewer {
.canvasWrapper {
overflow: hidden;
}
.page {
direction: ltr;
width: 816px;
height: 1056px;
margin: 1px auto -8px;
position: relative;
overflow: visible;
border: 9px solid transparent;
background-clip: content-box;
background-color: white;
canvas {
margin: 0;
display: block;
}
.adf-loadingIcon {
position: absolute;
display: block;
inset: 0;
width: 100px;
height: 100px;
left: 50%;
top: 50%;
margin-top: -50px;
margin-left: -50px;
font-size: 5px;
text-indent: -9999em;
border-top: 1.1em solid rgba(3, 0, 2, 0.2);
border-right: 1.1em solid rgba(3, 0, 2, 0.2);
border-bottom: 1.1em solid rgba(3, 0, 2, 0.2);
border-left: 1.1em solid #030002;
animation: load8 1.1s infinite linear;
border-radius: 50%;
&::after {
border-radius: 50%;
}
}
* {
padding: 0;
margin: 0;
}
}
&.adf-removePageBorders {
.adf-page {
margin: 0 auto 10px;
border: none;
}
}
.adf-pdf-viewer-annotation-tooltip.popup {
outline-color: var(--mat-sys-primary);
background-color: color-mix(in srgb, var(--mat-sys-primary) 30%, var(--mat-sys-on-primary));
margin-left: 40px;
width: max-content;
max-width: 300px;
opacity: 0;
pointer-events: none;
.popupContent {
display: inherit;
}
}
.textAnnotation {
&:hover,
&:focus-within {
/* stylelint-disable-next-line declaration-no-important */
z-index: 9999 !important; // important is required because pdfjs-dist adds style attribute to that tag
.adf-pdf-viewer-annotation-tooltip.popup {
opacity: 1;
pointer-events: auto;
}
}
}
}
.adf-hidden,
[hidden] {
display: none;
}
}
.adf-viewer-pdf-viewer {
overflow: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
inset: 0;
outline: none;
}
html[dir='ltr'] .adf-viewer-pdf-viewer {
box-shadow: inset 1px 0 0 hsla(0deg, 0%, 100%, 0.05);
}
html[dir='rtl'] .adf-viewer-pdf-viewer {
box-shadow: inset -1px 0 0 hsla(0deg, 0%, 100%, 0.05);
}
@@ -1,119 +0,0 @@
<div class="adf-pdf-viewer__container">
<ng-container *ngIf="showThumbnails">
<div class="adf-pdf-viewer__thumbnails">
<div class="adf-thumbnails-template__container">
<div class="adf-thumbnails-template__buttons">
<button mat-icon-button
data-automation-id='adf-thumbnails-close'
(click)="toggleThumbnails()"
[attr.aria-label]="'ADF_VIEWER.ARIA.THUMBNAILS_PANLEL_CLOSE' | translate"
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}">
<mat-icon adf-icon="close" />
</button>
</div>
<ng-container *ngIf="thumbnailsTemplate">
<ng-container *ngTemplateOutlet="thumbnailsTemplate;context:pdfThumbnailsContext" />
</ng-container>
<adf-pdf-thumbnails *ngIf="!thumbnailsTemplate && !isPanelDisabled"
(close)="toggleThumbnails()"
[pdfViewer]="pdfViewer" />
</div>
</div>
</ng-container>
<div class="adf-pdf-viewer__content">
<div [id]="randomPdfId + '-viewer-pdf-viewer'"
class="adf-viewer-pdf-viewer"
(window:resize)="onResize()">
<div [id]="randomPdfId + '-viewer-viewerPdf'"
class="adf-pdfViewer pdfViewer"
role="document"
tabindex="0"
aria-expanded="true">
<div id="loader-container" class="adf-loader-container">
<div class="adf-loader-item">
<mat-progress-bar [attr.aria-label]="'ADF_VIEWER.ARIA.LOADING' | translate"
class="adf-loader-item-progress-bar" mode="indeterminate" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="adf-pdf-viewer__toolbar" *ngIf="showToolbar" [ngStyle]="documentOverflow && {bottom: '25px'}">
<adf-toolbar>
<ng-container *ngIf="allowThumbnails">
<button mat-icon-button
[attr.aria-label]="'ADF_VIEWER.ARIA.THUMBNAILS' | translate"
[attr.aria-expanded]="showThumbnails"
data-automation-id="adf-thumbnails-button"
[disabled]="isPanelDisabled"
(click)="toggleThumbnails()">
<mat-icon adf-icon="dashboard" />
</button>
<adf-toolbar-divider />
</ng-container>
<button id="viewer-previous-page-button"
title="{{ 'ADF_VIEWER.ARIA.PREVIOUS_PAGE' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.PREVIOUS_PAGE' | translate }}"
mat-icon-button
(click)="previousPage()">
<mat-icon adf-icon="keyboard_arrow_up" />
</button>
<button id="viewer-next-page-button"
title="{{ 'ADF_VIEWER.ARIA.NEXT_PAGE' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.NEXT_PAGE' | translate }}"
mat-icon-button
(click)="nextPage()">
<mat-icon adf-icon="keyboard_arrow_down" />
</button>
<div class="adf-pdf-viewer__toolbar-page-selector">
<label for="page-selector">{{ 'ADF_VIEWER.PAGE_LABEL.SHOWING' | translate }}</label>
<input #page
id="page-selector"
type="text"
data-automation-id="adf-page-selector"
pattern="-?[0-9]*(\.[0-9]+)?"
value="{{ displayPage }}"
[attr.aria-label]="'ADF_VIEWER.PAGE_LABEL.PAGE_SELECTOR_LABEL' | translate"
(keyup.enter)="inputPage(page.value)">
<span>{{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}</span>
</div>
<div class="adf-pdf-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
{{ currentScaleText }}
</div>
<button id="viewer-zoom-in-button"
title="{{ 'ADF_VIEWER.ARIA.ZOOM_IN' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ZOOM_IN' | translate }}"
mat-icon-button
(click)="zoomIn()">
<mat-icon adf-icon="zoom_in" />
</button>
<button id="viewer-zoom-out-button"
title="{{ 'ADF_VIEWER.ARIA.ZOOM_OUT' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ZOOM_OUT' | translate }}"
mat-icon-button
(click)="zoomOut()">
<mat-icon adf-icon="zoom_out" />
</button>
<button id="viewer-scale-page-button"
role="button" aria-pressed="true"
title="{{ 'ADF_VIEWER.ARIA.FIT_PAGE' | translate }}"
attr.aria-label="{{ 'ADF_VIEWER.ARIA.FIT_PAGE' | translate }}"
mat-icon-button
(click)="pageFit()">
<mat-icon adf-icon="zoom_out_map" />
</button>
</adf-toolbar>
</div>
@@ -1,142 +0,0 @@
@use '../../../styles/mat-selectors' as ms;
.adf-pdf-viewer {
width: 100%;
height: 100%;
margin: 0;
.adf-loader-container {
display: flex;
flex-direction: row;
height: 100%;
}
&__thumbnails {
position: relative;
height: 100%;
width: 190px;
background-color: var(--mat-sys-surface-container-low);
display: flex;
flex-direction: column;
padding: 0;
.adf-info-drawer-layout {
display: flex;
flex-direction: column;
flex: 1;
background: var(--mat-sys-surface-container);
}
.adf-info-drawer-layout-header {
margin-bottom: 0;
}
.adf-info-drawer-layout-content {
padding: 0;
height: 100%;
overflow: hidden;
}
.adf-info-drawer-content {
height: 100%;
}
.adf-info-drawer-layout-content > *:last-child {
height: 100%;
overflow: hidden;
}
}
.adf-thumbnails-template {
&__container {
display: flex;
flex-direction: column;
height: 100%;
}
&__buttons {
height: 45px;
justify-content: flex-end;
align-items: flex-end;
display: flex;
}
}
&__container {
display: flex;
height: 100%;
min-height: 1px;
}
&__content {
flex: 1 1 auto;
position: relative;
}
.adf-loader-item {
margin: auto;
max-height: 100px;
max-width: 300px;
.adf-loader-item-progress-bar {
max-width: 300px;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
/* stylelint-disable-next-line property-no-vendor-prefix */
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
}
&__toolbar {
z-index: 100;
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
.adf-toolbar #{ms.$mat-toolbar} {
max-height: 48px;
background-color: var(--mat-sys-surface);
border-width: 0;
border-radius: 2px;
box-shadow:
0 2px 2px 0 rgba(0, 0, 0, 20.4),
0 0 2px 0 rgba(0, 0, 0, 0.12);
}
&-page-selector {
padding-left: 10px;
padding-right: 10px;
white-space: nowrap;
& > input {
border: 1px solid var(--mat-sys-outline-variant);
background-color: var(--mat-sys-surface);
color: inherit;
padding: 5px;
height: 24px;
line-height: 24px;
text-align: right;
width: 33px;
margin: 0 5px;
outline-width: 1px;
outline-color: var(--mat-sys-surface-container);
}
}
&-page-scale {
cursor: default;
width: 79px;
height: 24px;
font-size: var(--mat-sys-body-small-size);
border: 1px solid var(--mat-sys-outline-variant);
text-align: center;
line-height: 24px;
margin-left: 4px;
margin-right: 4px;
}
}
}
@@ -1,702 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
import { Component, SimpleChange, SimpleChanges, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { AppConfigService } from '../../../app-config';
import { EventMock } from '../../../mock';
import { UnitTestingUtils, provideCoreAuthTesting } from '../../../testing';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
import { PDFJS_MODULE, PDFJS_VIEWER_MODULE, PdfViewerComponent } from './pdf-viewer.component';
import pdfjsLibraryMock, { annotations } from '../mock/pdfjs-lib.mock';
import { TranslateService } from '@ngx-translate/core';
declare const pdfjsLib: {
PasswordResponses: {
NEED_PASSWORD: number;
INCORRECT_PASSWORD: number;
};
};
@Component({
selector: 'adf-url-test-component',
imports: [PdfViewerComponent],
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [urlFile]="urlFile" /> `
})
class UrlTestComponent {
@ViewChild(PdfViewerComponent, { static: true })
pdfViewerComponent: PdfViewerComponent;
urlFile: string;
constructor() {
this.urlFile = './fake-test-file.pdf';
}
}
@Component({
selector: 'adf-url-test-password-component',
imports: [PdfViewerComponent],
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [urlFile]="urlFile" /> `
})
class UrlTestPasswordComponent {
@ViewChild(PdfViewerComponent, { static: true })
pdfViewerComponent: PdfViewerComponent;
urlFile: string;
constructor() {
this.urlFile = './fake-test-password-file.pdf';
}
}
@Component({
imports: [PdfViewerComponent],
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [blobFile]="blobFile" /> `
})
class BlobTestComponent {
@ViewChild(PdfViewerComponent, { static: true })
pdfViewerComponent: PdfViewerComponent;
blobFile: Blob;
constructor() {
this.blobFile = this.createFakeBlob();
}
createFakeBlob(): Blob {
const pdfData = atob(
'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' +
'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' +
'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' +
'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' +
'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' +
'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' +
'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' +
'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' +
'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' +
'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' +
'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' +
'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' +
'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G'
);
return new Blob([pdfData], { type: 'application/pdf' });
}
}
describe('Test PdfViewer component', () => {
let component: PdfViewerComponent;
let fixture: ComponentFixture<PdfViewerComponent>;
let change: SimpleChange;
let dialog: MatDialog;
let testingUtils: UnitTestingUtils;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [PdfViewerComponent],
providers: [
provideCoreAuthTesting(),
{
provide: MatDialog,
useValue: {
open: () => {}
}
},
RenderingQueueServices
]
});
fixture = TestBed.createComponent(PdfViewerComponent);
testingUtils = new UnitTestingUtils(fixture.debugElement);
dialog = TestBed.inject(MatDialog);
component = fixture.componentInstance;
component.showToolbar = true;
component.inputPage('1');
fixture.detectChanges();
await fixture.whenStable();
});
afterAll(() => {
fixture.destroy();
});
it('should Loader be present', () => {
expect(testingUtils.getByCSS('.adf-loader-container')).not.toBeNull();
});
describe('Required values', () => {
it('should thrown an error If urlFile is not present', () => {
change = new SimpleChange(null, null, true);
expect(() => {
component.ngOnChanges({ urlFile: change });
}).toThrow(new Error('Attribute urlFile or blobFile is required'));
});
it('should If blobFile is not present thrown an error ', () => {
change = new SimpleChange(null, null, true);
expect(() => {
component.ngOnChanges({ blobFile: change });
}).toThrow(new Error('Attribute urlFile or blobFile is required'));
});
});
describe('View with url file', () => {
let fixtureUrlTestComponent: ComponentFixture<UrlTestComponent>;
let elementUrlTestComponent: HTMLElement;
beforeEach(async () => {
fixtureUrlTestComponent = TestBed.createComponent(UrlTestComponent);
elementUrlTestComponent = fixtureUrlTestComponent.nativeElement;
testingUtils.setDebugElement(fixtureUrlTestComponent.debugElement);
fixtureUrlTestComponent.detectChanges();
await fixtureUrlTestComponent.whenStable();
});
afterEach(() => {
document.body.removeChild(elementUrlTestComponent);
});
it('should Canvas be present', async () => {
fixtureUrlTestComponent.detectChanges();
await fixtureUrlTestComponent.whenStable();
expect(testingUtils.getByCSS('.adf-pdfViewer')).not.toBeNull();
expect(testingUtils.getByCSS('.adf-viewer-pdf-viewer')).not.toBeNull();
});
it('should Input Page elements be present', async () => {
fixtureUrlTestComponent.detectChanges();
await fixtureUrlTestComponent.whenStable();
expect(testingUtils.getByCSS('.viewer-pagenumber-input')).toBeDefined();
expect(testingUtils.getByCSS('.viewer-total-pages')).toBeDefined();
expect(testingUtils.getByCSS('#viewer-previous-page-button')).not.toBeNull();
expect(testingUtils.getByCSS('#viewer-next-page-button')).not.toBeNull();
});
it('should Toolbar be hide if showToolbar is false', async () => {
component.showToolbar = false;
fixtureUrlTestComponent.detectChanges();
await fixtureUrlTestComponent.whenStable();
expect(testingUtils.getByCSS('.viewer-toolbar-command')).toBeNull();
expect(testingUtils.getByCSS('.viewer-toolbar-pagination')).toBeNull();
});
});
describe('View with blob file', () => {
let fixtureBlobTestComponent: ComponentFixture<BlobTestComponent>;
let elementBlobTestComponent: HTMLElement;
beforeEach(async () => {
fixtureBlobTestComponent = TestBed.createComponent(BlobTestComponent);
elementBlobTestComponent = fixtureBlobTestComponent.nativeElement;
testingUtils.setDebugElement(fixtureBlobTestComponent.debugElement);
fixtureBlobTestComponent.detectChanges();
await fixtureBlobTestComponent.whenStable();
});
afterEach(() => {
document.body.removeChild(elementBlobTestComponent);
});
it('should Canvas be present', async () => {
fixtureBlobTestComponent.detectChanges();
await fixtureBlobTestComponent.whenStable();
expect(testingUtils.getByCSS('.adf-pdfViewer')).not.toBeNull();
expect(testingUtils.getByCSS('.adf-viewer-pdf-viewer')).not.toBeNull();
});
it('should Next an Previous Buttons be present', async () => {
fixtureBlobTestComponent.detectChanges();
await fixtureBlobTestComponent.whenStable();
expect(testingUtils.getByCSS('#viewer-previous-page-button')).not.toBeNull();
expect(testingUtils.getByCSS('#viewer-next-page-button')).not.toBeNull();
});
it('should Input Page elements be present', async () => {
fixtureBlobTestComponent.detectChanges();
await fixtureBlobTestComponent.whenStable();
/* cspell:disable-next-line */
expect(testingUtils.getByCSS('.adf-viewer-pagenumber-input')).toBeDefined();
expect(testingUtils.getByCSS('.adf-viewer-total-pages')).toBeDefined();
expect(testingUtils.getByCSS('#viewer-previous-page-button')).not.toBeNull();
expect(testingUtils.getByCSS('#viewer-next-page-button')).not.toBeNull();
});
it('should Toolbar be hide if showToolbar is false', async () => {
fixtureBlobTestComponent.componentInstance.pdfViewerComponent.showToolbar = false;
fixtureBlobTestComponent.detectChanges();
await fixtureBlobTestComponent.whenStable();
expect(testingUtils.getByCSS('.viewer-toolbar-command')).toBeNull();
expect(testingUtils.getByCSS('.viewer-toolbar-pagination')).toBeNull();
});
});
describe('Password protection dialog', () => {
let fixtureUrlTestPasswordComponent: ComponentFixture<UrlTestPasswordComponent>;
let componentUrlTestPasswordComponent: UrlTestPasswordComponent;
describe('Open password dialog', () => {
beforeEach(async () => {
fixtureUrlTestPasswordComponent = TestBed.createComponent(UrlTestPasswordComponent);
componentUrlTestPasswordComponent = fixtureUrlTestPasswordComponent.componentInstance;
spyOn(dialog, 'open').and.callFake((_dialogComponent: unknown, context: { data: { reason: number } }) => {
if (context.data.reason === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
return {
afterClosed: () => of('wrong_password')
} as ReturnType<MatDialog['open']>;
}
if (context.data.reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
return {
afterClosed: () => of('password')
} as ReturnType<MatDialog['open']>;
}
return undefined;
});
fixtureUrlTestPasswordComponent.detectChanges();
await fixtureUrlTestPasswordComponent.whenStable();
});
afterEach(() => {
document.body.removeChild(fixtureUrlTestPasswordComponent.nativeElement);
});
it('should try to access protected pdf', async () => {
componentUrlTestPasswordComponent.pdfViewerComponent.onPdfPassword(() => {}, pdfjsLib.PasswordResponses.NEED_PASSWORD);
fixture.detectChanges();
await fixture.whenStable();
expect(dialog.open).toHaveBeenCalledTimes(1);
});
it('should raise dialog asking for password', async () => {
componentUrlTestPasswordComponent.pdfViewerComponent.onPdfPassword(() => {}, pdfjsLib.PasswordResponses.NEED_PASSWORD);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(dialog.open['calls'].all()[0].args[1].data).toEqual({
reason: pdfjsLib.PasswordResponses.NEED_PASSWORD
});
});
it('it should raise dialog with incorrect password', async () => {
componentUrlTestPasswordComponent.pdfViewerComponent.onPdfPassword(() => {}, pdfjsLib.PasswordResponses.INCORRECT_PASSWORD);
fixture.detectChanges();
await fixture.whenStable();
expect(dialog.open['calls'].all()[0].args[1].data).toEqual({
reason: pdfjsLib.PasswordResponses.INCORRECT_PASSWORD
});
});
});
describe('Close password dialog ', () => {
beforeEach(async () => {
fixtureUrlTestPasswordComponent = TestBed.createComponent(UrlTestPasswordComponent);
componentUrlTestPasswordComponent = fixtureUrlTestPasswordComponent.componentInstance;
spyOn(dialog, 'open').and.callFake(
() =>
({
afterClosed: () => of('')
}) as ReturnType<MatDialog['open']>
);
spyOn(componentUrlTestPasswordComponent.pdfViewerComponent.close, 'emit');
fixtureUrlTestPasswordComponent.detectChanges();
await fixtureUrlTestPasswordComponent.whenStable();
});
afterEach(() => {
document.body.removeChild(fixtureUrlTestPasswordComponent.nativeElement);
});
it('should try to access protected pdf', async () => {
componentUrlTestPasswordComponent.pdfViewerComponent.onPdfPassword(() => {}, pdfjsLib.PasswordResponses.NEED_PASSWORD);
fixture.detectChanges();
await fixture.whenStable();
expect(componentUrlTestPasswordComponent.pdfViewerComponent.close.emit).toHaveBeenCalledWith();
});
});
});
});
describe('Test PdfViewer - Zoom customization', () => {
let fixture: ComponentFixture<PdfViewerComponent>;
let component: PdfViewerComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [PdfViewerComponent],
providers: [
provideCoreAuthTesting(),
{
provide: MatDialog,
useValue: {
open: () => {}
}
},
RenderingQueueServices
]
});
fixture = TestBed.createComponent(PdfViewerComponent);
component = fixture.componentInstance;
});
afterAll(() => {
fixture.destroy();
});
it('should use the custom zoom if it is present in the app.config', () => {
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config['adf-viewer.pdf-viewer-scaling'] = 80;
expect(component.getUserScaling()).toBe(0.8);
});
it('should use the minimum scale zoom if the value given in app.config is less than the minimum allowed scale', () => {
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config['adf-viewer.pdf-viewer-scaling'] = 10;
fixture.detectChanges();
expect(component.getUserScaling()).toBe(0.25);
});
it('should use the maximum scale zoom if the value given in app.config is greater than the maximum allowed scale', () => {
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config['adf-viewer.pdf-viewer-scaling'] = 5555;
fixture.detectChanges();
expect(component.getUserScaling()).toBe(10);
});
});
describe('Test PdfViewer - User interaction', () => {
let fixture: ComponentFixture<PdfViewerComponent>;
let component: PdfViewerComponent;
let testingUtils: UnitTestingUtils;
let pdfViewerSpy: jasmine.Spy;
beforeEach(fakeAsync(() => {
pdfViewerSpy = jasmine.createSpy('PDFViewer').and.returnValue({
setDocument: jasmine.createSpy().and.returnValue({
loadingTask: () => ({
destroy: () => Promise.resolve()
}),
promise: new Promise((resolve) => {
resolve({
numPages: 6,
getPage: () => 'fakePage'
});
})
}),
forceRendering: jasmine.createSpy(),
update: jasmine.createSpy(),
currentScaleValue: 1,
_currentPageNumber: 1,
_pages: [{ width: 100, height: 100, scale: 1 }]
});
TestBed.configureTestingModule({
imports: [PdfViewerComponent],
providers: [
provideCoreAuthTesting(),
{
provide: MatDialog,
useValue: {
open: () => {}
}
},
RenderingQueueServices,
{ provide: PDFJS_VIEWER_MODULE, useValue: pdfViewerSpy },
{ provide: PDFJS_MODULE, useValue: pdfjsLibraryMock }
]
});
fixture = TestBed.createComponent(PdfViewerComponent);
component = fixture.componentInstance;
testingUtils = new UnitTestingUtils(fixture.debugElement);
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config['adf-viewer.pdf-viewer-scaling'] = 10;
component['setupPdfJsWorker'] = () => Promise.resolve();
component.urlFile = './fake-test-file.pdf';
fixture.detectChanges();
component.ngOnChanges({
urlFile: new SimpleChange(null, './fake-test-file.pdf', true)
});
flush();
}));
afterAll(() => {
fixture.destroy();
});
it('should init the viewer with annotation mode enabled', () => {
expect(pdfViewerSpy).toHaveBeenCalledWith(jasmine.objectContaining({ annotationMode: 1 }));
});
it('should Total number of pages be loaded', () => {
expect(component.totalPages).toBe(6);
});
it('should nextPage move to the next page', () => {
testingUtils.clickByCSS('#viewer-next-page-button');
expect(component.displayPage).toBe(2);
});
it('should event RIGHT_ARROW keyboard change pages', () => {
fixture.detectChanges();
EventMock.keyDown(RIGHT_ARROW);
expect(component.displayPage).toBe(2);
});
it('should event LEFT_ARROW keyboard change pages', () => {
component.inputPage('2');
EventMock.keyDown(LEFT_ARROW);
expect(component.displayPage).toBe(1);
});
it('should previous page move to the previous page', () => {
testingUtils.clickByCSS('#viewer-next-page-button');
testingUtils.clickByCSS('#viewer-next-page-button');
testingUtils.clickByCSS('#viewer-previous-page-button');
expect(component.displayPage).toBe(2);
});
it('should previous page not move to the previous page if is page 1', () => {
component.previousPage();
expect(component.displayPage).toBe(1);
});
it('should Input page move to the inserted page', () => {
component.inputPage('2');
expect(component.displayPage).toBe(2);
});
it('should configure PDF.js with the correct wasmUrl', () => {
const changes: SimpleChanges = {
blobFile: new SimpleChange(null, component.blobFile, true)
};
component.ngOnChanges(changes);
const getDocumentSpy = pdfjsLibraryMock.getDocument;
expect(getDocumentSpy).toHaveBeenCalled();
const loadingArgs = getDocumentSpy.calls.mostRecent().args[0];
expect(loadingArgs.wasmUrl).toBe('./wasm/');
});
describe('Zoom', () => {
it('should zoom in increment the scale value', () => {
const zoomBefore = component.pdfViewer.currentScaleValue;
testingUtils.clickByCSS('#viewer-zoom-in-button');
expect(component.currentScaleMode).toBe('auto');
const currentZoom = component.pdfViewer.currentScaleValue;
expect(zoomBefore < currentZoom).toBe(true);
});
it('should zoom out decrement the scale value', () => {
testingUtils.clickByCSS('#viewer-zoom-in-button');
const zoomBefore = component.pdfViewer.currentScaleValue;
testingUtils.clickByCSS('#viewer-zoom-out-button');
expect(component.currentScaleMode).toBe('auto');
const currentZoom = component.pdfViewer.currentScaleValue;
expect(zoomBefore > currentZoom).toBe(true);
});
it('should it-in button toggle page-fit and auto scale mode', fakeAsync(() => {
tick(250);
expect(component.currentScaleMode).toBe('init');
testingUtils.clickByCSS('#viewer-scale-page-button');
expect(component.currentScaleMode).toBe('page-fit');
testingUtils.clickByCSS('#viewer-scale-page-button');
expect(component.currentScaleMode).toBe('auto');
testingUtils.clickByCSS('#viewer-scale-page-button');
expect(component.currentScaleMode).toBe('page-fit');
}), 300);
});
describe('Resize interaction', () => {
it('should resize event trigger setScaleUpdatePages', () => {
spyOn(component, 'onResize');
EventMock.resizeMobileView();
expect(component.onResize).toHaveBeenCalled();
});
});
describe('Thumbnails', () => {
it('should have own context', () => {
expect(component.pdfThumbnailsContext.viewer).not.toBeNull();
});
it('should open thumbnails panel', () => {
expect(testingUtils.getByCSS('.adf-pdf-viewer__thumbnails')).toBeNull();
component.toggleThumbnails();
fixture.detectChanges();
expect(testingUtils.getByCSS('.adf-pdf-viewer__thumbnails')).not.toBeNull();
});
it('should not render PdfThumbListComponent during initialization of new pdfViewer', () => {
component.toggleThumbnails();
component.urlFile = 'file.pdf';
fixture.detectChanges();
expect(fixture.debugElement.query(By.directive(PdfThumbListComponent))).toBeNull();
});
});
describe('Viewer events', () => {
it('should react on the emit of pageChange event', () => {
const args = {
pageNumber: 6,
source: {
container: document.getElementById(`${component.randomPdfId}-viewer-pdf-viewer`)
}
};
component.onPageChange(args);
expect(component.displayPage).toBe(6);
expect(component.page).toBe(6);
});
it('should react on the emit of pagesLoaded event', () => {
expect(component.isPanelDisabled).toBe(true);
component.onPagesLoaded();
expect(component.isPanelDisabled).toBe(false);
});
});
describe('Annotations', () => {
const annotationImageAlt = 'Note Annotation';
const annotationAttribute = 'data-annotation-id';
let annotationElement: HTMLElement;
let annotationImageElement: HTMLImageElement;
let documentContainer: HTMLDivElement;
const dispatchAnnotationLayerRenderedEvent = (): void => {
pdfViewerSpy.calls.mostRecent().args[0].eventBus.dispatch('annotationlayerrendered', {
pageNumber: 1,
source: {
div: documentContainer
}
});
tick();
};
const getAnnotationPopupElement = (): HTMLElement => annotationElement.querySelector('.adf-pdf-viewer-annotation-tooltip');
const getAnnotationTitle = (): string => annotationElement.querySelector('.title').textContent;
const getAnnotationDate = (): string => annotationElement.querySelector('.popupDate')?.textContent;
const getAnnotationContent = (): string => annotationElement.querySelector('.popupContent').textContent;
beforeEach(() => {
documentContainer = document.createElement('div');
annotationImageElement = document.createElement('img');
annotationElement = document.createElement('section');
annotationElement.setAttribute(annotationAttribute, 'R13');
annotationElement.append(annotationImageElement);
documentContainer.append(annotationElement);
spyOn(TestBed.inject(TranslateService), 'instant').withArgs('ADF_VIEWER.ARIA.NOTE_ANNOTATION_IMG').and.returnValue(annotationImageAlt);
});
it('should have corrected image in annotation popup', fakeAsync(() => {
dispatchAnnotationLayerRenderedEvent();
expect(annotationImageElement.src).toBe(
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGV' +
'pZ2h0PSIyNCI+PHBhdGggZD0iTTIgMmgxNHYxNEgyeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0zIDNoMTJ2MTJIM3oiIGZpbGw9Ii' +
'NmZmRiMDAiLz48cGF0aCBkPSJNNSA1aDh2OGgtOHoiIGZpbGw9IiNmZmJiMDAiLz48L3N2Zz4='
);
expect(annotationImageElement.alt).toBe(annotationImageAlt);
}));
it('should have corrected content in annotation popup', fakeAsync(() => {
dispatchAnnotationLayerRenderedEvent();
expect(getAnnotationTitle()).toBe('Annotation title');
const dateText = getAnnotationDate();
// Date format may vary by locale, so check it contains the key parts
expect(dateText).toMatch(/2026/);
expect(dateText).toMatch(/10:41:06|10:41:6/);
expect(getAnnotationContent()).toBe('Annotation contents');
expect(getAnnotationPopupElement()).toBeDefined();
}));
it('should have corrected content in annotation popup if there is no modification date', fakeAsync(() => {
annotations[0].modificationDate = null;
dispatchAnnotationLayerRenderedEvent();
expect(getAnnotationTitle()).toBe('Annotation title');
expect(getAnnotationDate()).toBeUndefined();
expect(getAnnotationContent()).toBe('Annotation contents');
expect(getAnnotationPopupElement()).toBeDefined();
}));
it('should not have corrected content', fakeAsync(() => {
const annotationPopupElement = document.createElement('section');
annotationPopupElement.setAttribute(annotationAttribute, 'R1');
documentContainer.append(annotationPopupElement);
dispatchAnnotationLayerRenderedEvent();
expect(getAnnotationPopupElement()).toBeNull();
}));
});
});
@@ -1,759 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable no-underscore-dangle */
/* eslint-disable @angular-eslint/no-output-native */
import { NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
Component,
EventEmitter,
HostListener,
inject,
InjectionToken,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { from, Subject, switchMap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConfigService } from '../../../app-config';
import { ToolbarComponent, ToolbarDividerComponent } from '../../../toolbar';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { PdfPasswordDialogComponent } from '../pdf-viewer-password-dialog/pdf-viewer-password-dialog';
import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs';
import { PDFDateString } from 'pdfjs-dist/build/pdf.min.mjs';
import { EventBus, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs';
import { OnProgressParameters, PDFDocumentLoadingTask, PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist/types/src/display/api';
import { IconModule } from '../../../icon/icon.module';
export type PdfScaleMode = 'init' | 'page-actual' | 'page-width' | 'page-height' | 'page-fit' | 'auto';
export interface PageChangingEvent {
pageNumber: number;
source?: {
container?: {
id?: string;
};
};
}
export interface PdfThumbnailPage {
id: number;
getWidth: () => number;
getHeight: () => number;
getPage: () => Promise<PDFPageProxy>;
}
export interface PdfAnnotationData {
titleObj?: {
str?: string;
};
modificationDate?: string;
}
export interface PdfAnnotationWithTitle extends PdfAnnotationData {
titleObj: {
str: string;
};
}
export const PDFJS_MODULE = new InjectionToken('PDFJS_MODULE', { factory: () => pdfjsLib });
export const PDFJS_VIEWER_MODULE = new InjectionToken('PDFJS_VIEWER_MODULE', { factory: () => PDFViewer });
@Component({
selector: 'adf-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer-host.component.scss', './pdf-viewer.component.scss'],
providers: [RenderingQueueServices],
host: { class: 'adf-pdf-viewer' },
imports: [
MatButtonModule,
IconModule,
TranslatePipe,
PdfThumbListComponent,
NgIf,
NgTemplateOutlet,
MatProgressBarModule,
NgStyle,
ToolbarComponent,
ToolbarDividerComponent
],
encapsulation: ViewEncapsulation.None
})
export class PdfViewerComponent implements OnChanges, OnDestroy {
@Input()
urlFile: string;
@Input()
blobFile: Blob;
@Input()
fileName: string;
@Input()
showToolbar: boolean = true;
@Input()
allowThumbnails = false;
@Input()
thumbnailsTemplate: TemplateRef<unknown> = null;
@Input()
cacheType: string = '';
@Output()
rendered = new EventEmitter<void>();
@Output()
error = new EventEmitter<void>();
@Output()
close = new EventEmitter<void>();
@Output()
pagesLoaded = new EventEmitter<void>();
page: number;
displayPage: number;
totalPages: number;
loadingPercent: number;
pdfViewer: PDFViewer;
pdfJsWorkerUrl: string;
pdfJsWorkerInstance: Worker;
currentScaleMode: PdfScaleMode = 'init';
MAX_AUTO_SCALE: number = 1.25;
DEFAULT_SCALE_DELTA: number = 1.1;
MIN_SCALE: number = 0.25;
MAX_SCALE: number = 10.0;
loadingTask: PDFDocumentLoadingTask;
isPanelDisabled = true;
showThumbnails: boolean = false;
pdfThumbnailsContext: { viewer: PDFViewer | null } = { viewer: null };
randomPdfId: string;
documentOverflow = false;
get currentScaleText(): string {
const currentScaleValueStr = this.pdfViewer?.currentScaleValue;
const scaleNumber = Number(currentScaleValueStr);
const currentScaleText = scaleNumber ? `${Math.round(scaleNumber * 100)}%` : '';
return currentScaleText;
}
private readonly pdfjsLib = inject(PDFJS_MODULE);
private readonly pdfjsViewer = inject(PDFJS_VIEWER_MODULE);
private readonly eventBus = new EventBus();
private readonly pdfjsDefaultOptions = {
disableAutoFetch: true,
disableStream: true,
cMapUrl: './cmaps/',
cMapPacked: true,
wasmUrl: './wasm/'
};
private readonly pdfjsWorkerDestroy$ = new Subject<boolean>();
private readonly dialog = inject(MatDialog);
private readonly renderingQueueServices = inject(RenderingQueueServices);
private readonly appConfigService = inject(AppConfigService);
private readonly translateService = inject(TranslateService);
constructor() {
// needed to preserve "this" context
this.onPageChange = this.onPageChange.bind(this);
this.onPagesLoaded = this.onPagesLoaded.bind(this);
this.onPageRendered = this.onPageRendered.bind(this);
this.randomPdfId = Date.now().toString();
this.pdfjsWorkerDestroy$
.pipe(
catchError(() => null),
switchMap(() => from(this.destroyPfdJsWorker()))
)
.subscribe(() => {});
}
getUserScaling(): number {
let scaleConfig = this.appConfigService.get<number>('adf-viewer.pdf-viewer-scaling', undefined);
if (scaleConfig) {
scaleConfig = scaleConfig / 100;
scaleConfig = this.checkLimits(scaleConfig);
}
return scaleConfig;
}
checkLimits(scaleConfig: number): number {
if (scaleConfig > this.MAX_SCALE) {
return this.MAX_SCALE;
} else if (scaleConfig < this.MIN_SCALE) {
return this.MIN_SCALE;
} else {
return scaleConfig;
}
}
ngOnChanges(changes: SimpleChanges) {
const blobFile = changes['blobFile'];
if (blobFile?.currentValue) {
const reader = new FileReader();
reader.onload = async () => {
const pdfOptions = {
...this.pdfjsDefaultOptions,
data: reader.result,
withCredentials: this.appConfigService.get<boolean>('auth.withCredentials', undefined),
isEvalSupported: false
};
this.executePdf(pdfOptions);
};
reader.readAsArrayBuffer(blobFile.currentValue);
}
const urlFile = changes['urlFile'];
if (urlFile?.currentValue) {
const pdfOptions: {
url: string;
withCredentials: boolean | undefined;
isEvalSupported: boolean;
httpHeaders?: { 'Cache-Control': string };
} & typeof this.pdfjsDefaultOptions = {
...this.pdfjsDefaultOptions,
url: urlFile.currentValue,
withCredentials: this.appConfigService.get<boolean>('auth.withCredentials', undefined),
isEvalSupported: false
};
if (this.cacheType) {
pdfOptions.httpHeaders = {
'Cache-Control': this.cacheType
};
}
this.executePdf(pdfOptions);
}
if (!this.urlFile && !this.blobFile) {
throw new Error('Attribute urlFile or blobFile is required');
}
}
executePdf(pdfOptions: Parameters<typeof this.pdfjsLib.getDocument>[0]) {
this.setupPdfJsWorker().then(() => {
this.loadingTask = this.pdfjsLib.getDocument(pdfOptions);
this.loadingTask.onPassword = (callback, reason) => {
this.onPdfPassword(callback, reason);
};
this.loadingTask.onProgress = (progressData: OnProgressParameters) => {
const level = progressData.loaded / progressData.total;
this.loadingPercent = Math.round(level * 100);
};
this.isPanelDisabled = true;
this.loadingTask.promise
.then((pdfDocument) => {
this.totalPages = pdfDocument.numPages;
this.page = 1;
this.displayPage = 1;
this.initPDFViewer(pdfDocument);
return pdfDocument.getPage(1);
})
.then(() => {
setTimeout(() => this.scalePage('init'));
})
.catch(() => this.error.emit());
});
}
private async setupPdfJsWorker(): Promise<void> {
if (this.pdfJsWorkerInstance) {
await this.destroyPfdJsWorker();
} else if (!this.pdfJsWorkerUrl) {
this.pdfJsWorkerUrl = await this.getPdfJsWorker();
}
this.pdfJsWorkerInstance = new Worker(this.pdfJsWorkerUrl, { type: 'module' });
this.pdfjsLib.GlobalWorkerOptions.workerPort = this.pdfJsWorkerInstance;
}
private async getPdfJsWorker(): Promise<string> {
const response = await fetch('./pdf.worker.min.mjs');
const workerScript = await response.text();
const blob = new Blob([workerScript], { type: 'application/javascript' });
return URL.createObjectURL(blob);
}
initPDFViewer(pdfDocument: PDFDocumentProxy) {
const viewer: HTMLDivElement = this.getViewer();
const container = this.getDocumentContainer();
if (viewer && container) {
this.pdfViewer = new this.pdfjsViewer({
container,
viewer,
renderingQueue: this.renderingQueueServices,
eventBus: this.eventBus,
annotationMode: 1
});
// cspell: disable-next
this.eventBus.on('pagechanging', this.onPageChange);
// cspell: disable-next
this.eventBus.on('pagesloaded', this.onPagesLoaded);
// cspell: disable-next
this.eventBus.on('textlayerrendered', () => {
this.onPageRendered();
});
this.eventBus.on('pagerendered', () => {
this.onPageRendered();
});
this.eventBus.on('annotationlayerrendered', (event) =>
this.handleNotRecognizedAnnotations(pdfDocument, event.source.div, event.pageNumber)
);
this.renderingQueueServices.setViewer(this.pdfViewer);
this.pdfViewer.setDocument(pdfDocument);
this.pdfThumbnailsContext.viewer = this.pdfViewer;
}
}
ngOnDestroy() {
if (this.pdfViewer) {
// cspell: disable-next
this.eventBus.off('pagechanging', () => {});
// cspell: disable-next
this.eventBus.off('pagesloaded', () => {});
// cspell: disable-next
this.eventBus.off('textlayerrendered', () => {});
}
if (this.loadingTask) {
this.pdfjsWorkerDestroy$.next(true);
}
this.pdfjsWorkerDestroy$.complete();
this.revokePdfJsWorkerUrl();
}
private async destroyPfdJsWorker() {
if (this.loadingTask.destroy) {
await this.loadingTask.destroy();
}
if (this.pdfJsWorkerInstance) {
this.pdfJsWorkerInstance.terminate();
}
this.loadingTask = null;
}
private revokePdfJsWorkerUrl(): void {
URL.revokeObjectURL(this.pdfJsWorkerUrl);
}
toggleThumbnails() {
this.showThumbnails = !this.showThumbnails;
}
/**
* Method to scale the page current support implementation
*
* @param scaleMode - new scale mode
*/
scalePage(scaleMode: PdfScaleMode) {
this.currentScaleMode = scaleMode;
const viewerContainer = this.getMainContainer();
const documentContainer = this.getDocumentContainer();
if (this.pdfViewer && documentContainer) {
let widthContainer: number;
let heightContainer: number;
if (viewerContainer && viewerContainer.clientWidth <= documentContainer.clientWidth) {
widthContainer = viewerContainer.clientWidth;
heightContainer = viewerContainer.clientHeight;
} else {
widthContainer = documentContainer.clientWidth;
heightContainer = documentContainer.clientHeight;
}
const currentPage = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1];
const padding = 20;
const pageWidthScale = ((widthContainer - padding) / currentPage.width) * currentPage.scale;
const pageHeightScale = ((heightContainer - padding) / currentPage.width) * currentPage.scale;
let scale: number;
switch (this.currentScaleMode) {
case 'init':
case 'page-fit': {
scale = this.getUserScaling();
if (!scale) {
scale = this.autoScaling(pageHeightScale, pageWidthScale);
}
break;
}
case 'page-actual': {
scale = 1;
break;
}
case 'page-width': {
scale = pageWidthScale;
break;
}
case 'page-height': {
scale = pageHeightScale;
break;
}
case 'auto': {
scale = this.autoScaling(pageHeightScale, pageWidthScale);
break;
}
default:
return;
}
this.setScaleUpdatePages(scale);
}
}
private autoScaling(pageHeightScale: number, pageWidthScale: number) {
let horizontalScale: number;
if (this.isLandscape) {
horizontalScale = Math.min(pageHeightScale, pageWidthScale);
} else {
horizontalScale = pageWidthScale;
}
horizontalScale = Math.round(horizontalScale);
const scale = Math.min(this.MAX_AUTO_SCALE, horizontalScale);
return this.checkPageFitInContainer(scale);
}
private getMainContainer(): HTMLElement {
return document.getElementById(`${this.randomPdfId}-viewer-main-container`);
}
private getDocumentContainer(): HTMLDivElement {
return document.getElementById(`${this.randomPdfId}-viewer-pdf-viewer`) as HTMLDivElement;
}
private getViewer(): HTMLDivElement {
return document.getElementById(`${this.randomPdfId}-viewer-viewerPdf`) as HTMLDivElement;
}
checkPageFitInContainer(scale: number): number {
const documentContainerSize = this.getDocumentContainer();
const page = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1];
if (page.width > documentContainerSize.clientWidth) {
scale = Math.fround((documentContainerSize.clientWidth - 20) / page.width);
if (scale < this.MIN_SCALE) {
scale = this.MIN_SCALE;
}
}
return scale;
}
setDocumentOverflow() {
const documentContainerSize = this.getDocumentContainer();
const page = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1];
this.documentOverflow = page.width > documentContainerSize.clientWidth;
}
/**
* Update all the pages with the newScale scale
*
* @param newScale - new scale page
*/
setScaleUpdatePages(newScale: number) {
if (this.pdfViewer) {
if (!this.isSameScale(this.pdfViewer.currentScaleValue, newScale.toString())) {
this.pdfViewer.currentScaleValue = newScale.toString();
}
this.pdfViewer.update();
}
this.setDocumentOverflow();
}
/**
* Check if the request scale of the page is the same for avoid useless re-rendering
*
* @param oldScale - old scale page
* @param newScale - new scale page
* @returns `true` if the scale is the same, otherwise `false`
*/
isSameScale(oldScale: string, newScale: string): boolean {
return newScale === oldScale;
}
/**
* Check if is a land scape view
*
* @param width target width
* @param height target height
* @returns `true` if the target is in the landscape mode, otherwise `false`
*/
isLandscape(width: number, height: number): boolean {
return width > height;
}
/**
* Method triggered when the page is resized
*/
onResize() {
this.scalePage(this.currentScaleMode);
}
/**
* toggle the fit page pdf
*/
pageFit() {
if (this.currentScaleMode !== 'page-fit') {
this.scalePage('page-fit');
} else {
this.scalePage('auto');
}
}
/**
* zoom in page pdf
*
* @param ticks number of ticks to zoom
*/
zoomIn(ticks?: number): void {
let newScale: number = Number(this.pdfViewer.currentScaleValue);
do {
newScale = Number((newScale * this.DEFAULT_SCALE_DELTA).toFixed(2));
newScale = Math.ceil(newScale * 10) / 10;
newScale = Math.min(this.MAX_SCALE, newScale);
} while (--ticks > 0 && newScale < this.MAX_SCALE);
this.currentScaleMode = 'auto';
this.setScaleUpdatePages(newScale);
}
/**
* zoom out page pdf
*
* @param ticks number of ticks to scale
*/
zoomOut(ticks?: number): void {
let newScale: number = Number(this.pdfViewer.currentScaleValue);
do {
newScale = Number((newScale / this.DEFAULT_SCALE_DELTA).toFixed(2));
newScale = Math.floor(newScale * 10) / 10;
newScale = Math.max(this.MIN_SCALE, newScale);
} while (--ticks > 0 && newScale > this.MIN_SCALE);
this.currentScaleMode = 'auto';
this.setScaleUpdatePages(newScale);
}
/**
* load the previous page
*/
previousPage() {
if (this.pdfViewer && this.page > 1) {
this.page--;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
}
}
/**
* load the next page
*/
nextPage() {
if (this.pdfViewer && this.page < this.totalPages) {
this.page++;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
}
}
/**
* load the page in input
*
* @param page to load
*/
inputPage(page: string) {
const pageInput = parseInt(page, 10);
if (!isNaN(pageInput) && pageInput > 0 && pageInput <= this.totalPages) {
this.page = pageInput;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
} else {
this.displayPage = this.page;
}
}
/**
* Page Change Event
*
* @param event - page change event
* @param event.pageNumber - the new page number
* @param event.source - the source object
* @param event.source.container - the container element
* @param event.source.container.id - the container id
*/
onPageChange(event: PageChangingEvent) {
if (event.source && event.source.container.id === `${this.randomPdfId}-viewer-pdf-viewer`) {
this.page = event.pageNumber;
this.displayPage = event.pageNumber;
}
}
onPdfPassword(callback, reason) {
this.dialog
.open(PdfPasswordDialogComponent, {
width: '400px',
data: { reason }
})
.afterClosed()
.subscribe((password) => {
if (password) {
callback(password);
} else {
this.close.emit();
}
});
}
/**
* Page Rendered Event
*/
onPageRendered() {
this.rendered.emit();
}
/**
* Pages Loaded Event
*
*/
onPagesLoaded() {
this.isPanelDisabled = false;
this.pagesLoaded.emit();
}
/**
* Keyboard Event Listener
*
* @param event KeyboardEvent
*/
@HostListener('document:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
const key = event.keyCode;
if (key === 39) {
// right arrow
this.nextPage();
} else if (key === 37) {
// left arrow
this.previousPage();
}
}
private async handleNotRecognizedAnnotations(
pdfDocument: PDFDocumentProxy,
documentContainer: HTMLDivElement,
pageNumber: number
): Promise<void> {
const page = await pdfDocument.getPage(pageNumber);
const annotations = await page.getAnnotations();
annotations.forEach((annotation) => {
if (annotation.subtype !== 'Text' || annotation.name !== 'NoIcon') {
return;
}
const annotationElement = documentContainer.querySelector<HTMLElement>(`[data-annotation-id="${annotation.id}"]`);
if (!annotationElement) {
return;
}
this.correctAnnotationImage(annotationElement);
const text: string = annotation.contentsObj?.str?.trim();
if (!text || (annotation.popupRef && documentContainer.querySelector(`[data-annotation-id="${annotation.popupRef}"]`))) {
return;
}
this.createAnnotationPopup(annotation, text, annotationElement);
});
}
private correctAnnotationImage(annotationElement: HTMLElement): void {
const annotationImageElement = annotationElement.querySelector('img');
if (annotationImageElement) {
annotationImageElement.src =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGV' +
'pZ2h0PSIyNCI+PHBhdGggZD0iTTIgMmgxNHYxNEgyeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0zIDNoMTJ2MTJIM3oiIGZpbGw9Ii' +
'NmZmRiMDAiLz48cGF0aCBkPSJNNSA1aDh2OGgtOHoiIGZpbGw9IiNmZmJiMDAiLz48L3N2Zz4=';
annotationImageElement.alt = this.translateService.instant('ADF_VIEWER.ARIA.NOTE_ANNOTATION_IMG');
}
}
private createAnnotationPopup(annotation: PdfAnnotationData, text: string, annotationElement: HTMLElement): void {
const popupElement = document.createElement('div');
let headerElement: HTMLSpanElement;
if (annotation.titleObj?.str) {
headerElement = this.createAnnotationPopupHeader(annotation as PdfAnnotationWithTitle);
}
const contentElement = this.createAnnotationPopupContent(text);
popupElement.classList.add('popup', 'adf-pdf-viewer-annotation-tooltip');
headerElement ? popupElement.append(headerElement, contentElement) : popupElement.append(contentElement);
annotationElement.appendChild(popupElement);
}
private createAnnotationPopupHeader(annotation: PdfAnnotationWithTitle): HTMLSpanElement {
const headerElement = document.createElement('span');
const titleElement = document.createElement('span');
let dateElement: HTMLTimeElement;
titleElement.innerText = annotation.titleObj.str;
titleElement.classList.add('title');
headerElement.classList.add('header');
if (annotation.modificationDate) {
dateElement = document.createElement('time');
dateElement.innerText = PDFDateString.toDateObject(annotation.modificationDate).toLocaleString();
dateElement.classList.add('popupDate');
headerElement.append(titleElement, dateElement);
} else {
headerElement.append(titleElement);
}
return headerElement;
}
private createAnnotationPopupContent(text: string): HTMLSpanElement {
const contentElement = document.createElement('span');
contentElement.innerText = text;
contentElement.classList.add('popupContent');
return contentElement;
}
}
@@ -32,17 +32,13 @@
}
@case ('pdf') {
<adf-pdf-viewer
[thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFile"
[fileName]="internalFileName"
[cacheType]="cacheTypeForContent"
(pagesLoaded)="markAsLoaded()"
(close)="onClose()"
(error)="onUnsupportedFile()"
/>
@if (pdfViewerComponent) {
<ng-container
[ngComponentOutlet]="pdfViewerComponent"
[ngComponentOutletInputs]="pdfViewerInputs" />
} @else {
<adf-viewer-unknown-format />
}
}
@case ('image') {
@@ -18,13 +18,30 @@
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing';
import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { Component, DebugElement, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, DeferBlockBehavior, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { UnitTestingUtils } from '../../../testing';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { ViewerRenderComponent } from './viewer-render.component';
import { ImgViewerComponent, MediaPlayerComponent, PdfViewerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token';
import { PdfViewerRef } from '../../tokens/pdf-viewer-ref';
import { ImgViewerComponent, MediaPlayerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
@Component({
selector: 'adf-pdf-viewer',
template: '<div class="adf-pdf-viewer-mock"></div>'
})
class MockPdfViewerComponent implements PdfViewerRef {
@Input() urlFile = '';
@Input() blobFile: Blob;
@Input() fileName = '';
@Input() allowThumbnails = false;
@Input() thumbnailsTemplate: TemplateRef<unknown> = null;
@Input() cacheType = '';
@Output() pagesLoaded = new EventEmitter<void>();
@Output() error = new EventEmitter<void>();
@Output() close = new EventEmitter<void>();
}
@Component({
selector: 'adf-double-viewer',
@@ -69,7 +86,7 @@ describe('ViewerComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MatDialogModule, ViewerRenderComponent, DoubleViewerComponent],
providers: [RenderingQueueServices, { provide: Location, useClass: SpyLocation }, MatDialog],
providers: [{ provide: Location, useClass: SpyLocation }, MatDialog, { provide: PDF_VIEWER_COMPONENT, useValue: MockPdfViewerComponent }],
deferBlockBehavior: DeferBlockBehavior.Playthrough
});
fixture = TestBed.createComponent(ViewerRenderComponent);
@@ -532,7 +549,7 @@ describe('ViewerComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
const imgViewer = testingUtils.getByDirective(PdfViewerComponent);
const imgViewer = testingUtils.getByDirective(MockPdfViewerComponent);
imgViewer.triggerEventHandler('pagesLoaded', null);
fixture.detectChanges();
@@ -16,9 +16,10 @@
*/
import { AppExtensionService, ExtensionsModule, ViewerExtensionRef, PreviewExtensionComponent } from '@alfresco/adf-extensions';
import { NgForOf, NgTemplateOutlet } from '@angular/common';
import { NgComponentOutlet, NgForOf, NgTemplateOutlet } from '@angular/common';
import {
Component,
effect,
EventEmitter,
Injector,
Input,
@@ -26,18 +27,22 @@ import {
OnInit,
Output,
TemplateRef,
Type,
ViewChild,
ViewEncapsulation,
inject
inject,
viewChild
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslatePipe } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { Track } from '../../models/viewer.model';
import { ViewUtilService } from '../../services/view-util.service';
import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token';
import { PdfViewerRef } from '../../tokens/pdf-viewer-ref';
import { ImgViewerComponent } from '../img-viewer/img-viewer.component';
import { MediaPlayerComponent } from '../media-player/media-player.component';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { TxtViewerComponent } from '../txt-viewer/txt-viewer.component';
import { UnknownFormatComponent } from '../unknown-format/unknown-format.component';
@@ -50,7 +55,7 @@ import { UnknownFormatComponent } from '../unknown-format/unknown-format.compone
imports: [
TranslatePipe,
MatProgressSpinnerModule,
PdfViewerComponent,
NgComponentOutlet,
ImgViewerComponent,
MediaPlayerComponent,
TxtViewerComponent,
@@ -68,6 +73,30 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
dialog = inject(MatDialog);
readonly injector = inject(Injector);
readonly pdfViewerComponent: Type<PdfViewerRef> | null = inject(PDF_VIEWER_COMPONENT, { optional: true });
pdfViewerInputs: Record<string, unknown> = {};
private readonly pdfOutlet = viewChild(NgComponentOutlet);
constructor() {
effect((onCleanup) => {
const outlet = this.pdfOutlet();
const instance = outlet?.componentInstance as PdfViewerRef | null;
if (!instance) {
return;
}
const subs: Subscription[] = [
instance.pagesLoaded.subscribe(() => this.markAsLoaded()),
instance.close.subscribe(() => this.onClose()),
instance.error.subscribe(() => this.onUnsupportedFile())
];
onCleanup(() => subs.forEach((s) => s.unsubscribe()));
});
}
/**
* If you want to load an external file that does not come from ACS you
* can use this URL to specify where to load the file from.
@@ -197,6 +226,13 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
} else if (this.urlFile) {
this.setUpUrlFile();
}
if (this.viewerType === 'pdf' && !this.pdfViewerComponent) {
console.error(
'@alfresco/adf-core: PDF viewer is not configured. ' +
'Add providePdfViewer() from @alfresco/adf-core/viewer/pdf to your application providers.'
);
}
this.updatePdfViewerInputs();
}
markAsLoaded() {
@@ -250,4 +286,15 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
onClose() {
this.close.next(true);
}
private updatePdfViewerInputs(): void {
this.pdfViewerInputs = {
urlFile: this.urlFile,
blobFile: this.blobFile,
fileName: this.internalFileName,
allowThumbnails: this.allowThumbnails,
thumbnailsTemplate: this.thumbnailsTemplate,
cacheType: this.cacheTypeForContent
};
}
}
+2 -5
View File
@@ -16,13 +16,10 @@
*/
export * from './services/view-util.service';
export * from './tokens/pdf-viewer.token';
export * from './tokens/pdf-viewer-ref';
export * from './components/img-viewer/img-viewer.component';
export * from './components/media-player/media-player.component';
export * from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog';
export * from './components/pdf-viewer/pdf-viewer.component';
export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
export * from './components/pdf-viewer-thumb/pdf-viewer-thumb.component';
export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
export * from './components/txt-viewer/txt-viewer.component';
export * from './components/unknown-format/unknown-format.component';
export * from './components/viewer-more-actions.component';
@@ -1,228 +0,0 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import type { PDFViewer } from 'pdfjs-dist/types/web/pdf_viewer';
import type { PDFThumbnailViewer } from 'pdfjs-dist/types/web/pdf_thumbnail_viewer';
import type { PDFPageView } from 'pdfjs-dist/types/web/pdf_page_view';
interface VisiblePage {
id: number;
}
interface VisiblePages {
first?: VisiblePage;
last?: VisiblePage;
views?: Array<{ view: PDFPageView }>;
}
/**
* Type helper for accessing the resume method on PDFPageView.
* PDFPageView implements IRenderableView which includes a resume method,
* but the TypeScript definitions don't properly expose it.
*/
// cspell:ignore Renderable
interface ResumableView {
resume?: () => void;
}
/**
*
* RenderingQueueServices rendering of the views for pages and thumbnails.
*
*/
@Injectable()
export class RenderingQueueServices {
renderingStates = {
INITIAL: 0,
RUNNING: 1,
PAUSED: 2,
FINISHED: 3
};
CLEANUP_TIMEOUT: number = 30_000;
pdfViewer: PDFViewer | null = null;
pdfThumbnailViewer: PDFThumbnailViewer | null = null;
onIdle: (() => void) | null = null;
highestPriorityPage: string | null = null;
idleTimeout: number | null = null;
printing = false;
isThumbnailViewEnabled = false;
/**
* Set the instance of the PDF Viewer
*
* @param pdfViewer viewer instance
*/
setViewer(pdfViewer: PDFViewer): void {
this.pdfViewer = pdfViewer;
}
/**
* Sets the instance of the PDF Thumbnail Viewer
*
* @param pdfThumbnailViewer viewer instance
*/
setThumbnailViewer(pdfThumbnailViewer: PDFThumbnailViewer): void {
this.pdfThumbnailViewer = pdfThumbnailViewer;
}
/**
* Check if the view has highest rendering priority
*
* @param view view to render
* @returns `true` if the view has higher priority, otherwise `false`
*/
isHighestPriority(view: PDFPageView): boolean {
return this.highestPriorityPage === view.renderingId;
}
renderHighestPriority(currentlyVisiblePages?: unknown): void {
if (this.idleTimeout) {
clearTimeout(this.idleTimeout);
this.idleTimeout = null;
}
// Pages have a higher priority than thumbnails, so check them first.
if (this.pdfViewer?.forceRendering(currentlyVisiblePages)) {
return;
}
// No pages needed rendering so check thumbnails.
if (this.pdfThumbnailViewer && this.isThumbnailViewEnabled && this.pdfThumbnailViewer.forceRendering()) {
return;
}
if (this.printing) {
// If printing is currently ongoing do not reschedule cleanup.
return;
}
if (this.onIdle) {
// Type assertion needed: setTimeout returns NodeJS.Timeout in Node types,
// but returns number at runtime in browser (where this code executes).
// PDFRenderingQueue interface requires idleTimeout to be number.
this.idleTimeout = setTimeout(this.onIdle.bind(this), this.CLEANUP_TIMEOUT) as unknown as number;
}
}
/**
* Gets the highest priority page to render from the visible pages
* This method is part of the PDFRenderingQueue interface compatibility
*
* @param visible visible pages information
* @param views array of page views
* @param scrolledDown whether the user scrolled down
* @returns the highest priority page view to render, null if all done, or false if no visible pages
*/
getHighestPriority(visible: VisiblePages, views: PDFPageView[], scrolledDown: boolean): PDFPageView | null | false {
// The state has changed figure out which page has the highest priority to
// render next (if any).
// Priority:
// 1 visible pages
// 2 if last scrolled down page after the visible pages
// 2 if last scrolled up page before the visible pages
const visibleViews = visible.views;
if (!visibleViews) {
return false;
}
const numberVisible = visibleViews.length;
if (numberVisible === 0) {
return false;
}
for (let i = 0; i < numberVisible; ++i) {
const view = visibleViews[i].view;
if (!this.isViewFinished(view)) {
return view;
}
}
// All the visible views have rendered, try to render next/previous pages.
if (scrolledDown && visible.last) {
const nextPageIndex = visible.last.id;
// ID's start at 1 so no need to add 1.
if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex])) {
return views[nextPageIndex];
}
} else if (visible.first) {
const previousPageIndex = visible.first.id - 2;
if (views[previousPageIndex] && !this.isViewFinished(views[previousPageIndex])) {
return views[previousPageIndex];
}
}
// Everything that needs to be rendered has been.
return null;
}
hasViewer(): boolean {
return !!this.pdfViewer;
}
/**
* Checks if the view rendering is finished
*
* @param view the View instance to check
* @returns `true` if rendering is finished, otherwise `false`
*/
isViewFinished(view: PDFPageView): boolean {
return view.renderingState === this.renderingStates.FINISHED;
}
/**
* Render a page or thumbnail view. This calls the appropriate function
* based on the views state. If the view is already rendered it will return
* false.
*
* @param view View instance to render
* @returns the rendered state of the view
*/
renderView(view: PDFPageView): boolean {
const state = view.renderingState;
switch (state) {
case this.renderingStates.FINISHED: {
return false;
}
case this.renderingStates.PAUSED: {
this.highestPriorityPage = view.renderingId;
const resumableView = view as unknown as ResumableView;
if (resumableView.resume) {
resumableView.resume();
}
break;
}
case this.renderingStates.RUNNING: {
this.highestPriorityPage = view.renderingId;
break;
}
case this.renderingStates.INITIAL: {
this.highestPriorityPage = view.renderingId;
const continueRendering = () => {
this.renderHighestPriority();
};
view.draw().then(continueRendering, continueRendering);
break;
}
default: {
break;
}
}
return true;
}
}
@@ -0,0 +1,30 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter, TemplateRef } from '@angular/core';
export interface PdfViewerRef {
urlFile: string;
blobFile: Blob;
fileName: string;
allowThumbnails: boolean;
thumbnailsTemplate: TemplateRef<unknown>;
cacheType: string;
pagesLoaded: EventEmitter<void>;
error: EventEmitter<void>;
close: EventEmitter<void>;
}
@@ -0,0 +1,24 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InjectionToken, Type } from '@angular/core';
import { PdfViewerRef } from './pdf-viewer-ref';
export const PDF_VIEWER_COMPONENT = new InjectionToken<Type<PdfViewerRef> | null>('PDF_VIEWER_COMPONENT', {
providedIn: 'root',
factory: () => null
});
-8
View File
@@ -19,10 +19,6 @@ import { NgModule } from '@angular/core';
import { DownloadPromptDialogComponent } from './components/download-prompt-dialog/download-prompt-dialog.component';
import { ImgViewerComponent } from './components/img-viewer/img-viewer.component';
import { MediaPlayerComponent } from './components/media-player/media-player.component';
import { PdfPasswordDialogComponent } from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog';
import { PdfThumbComponent } from './components/pdf-viewer-thumb/pdf-viewer-thumb.component';
import { PdfThumbListComponent } from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component';
import { TxtViewerComponent } from './components/txt-viewer/txt-viewer.component';
import { UnknownFormatComponent } from './components/unknown-format/unknown-format.component';
import { ViewerMoreActionsComponent } from './components/viewer-more-actions.component';
@@ -36,14 +32,10 @@ import { ViewerComponent } from './components/viewer.component';
import { ViewerExtensionDirective } from './directives/viewer-extension.directive';
export const VIEWER_DIRECTIVES = [
PdfPasswordDialogComponent,
ViewerRenderComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
PdfViewerComponent,
PdfThumbComponent,
PdfThumbListComponent,
ViewerExtensionDirective,
UnknownFormatComponent,
ViewerToolbarComponent,