mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
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:
-31
@@ -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>
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
.adf-fill-remaining-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.adf-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-dialog-buttons button {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
-102
@@ -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');
|
||||
});
|
||||
});
|
||||
-58
@@ -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>
|
||||
-73
@@ -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;
|
||||
}
|
||||
}
|
||||
-13
@@ -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>
|
||||
-30
@@ -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);
|
||||
}
|
||||
}
|
||||
-236
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
-246
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user