mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-09-17 14:21:29 +00:00
AAE-34298 Move viewer to separate entry point
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<div mat-dialog-title>
|
||||
<h3>{{ 'ADF_VIEWER.NON_RESPONSIVE_DIALOG.HEADER' | translate }}</h3>
|
||||
</div>
|
||||
<mat-dialog-content>
|
||||
{{ 'ADF_VIEWER.NON_RESPONSIVE_DIALOG.LABEL' | translate }}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
mat-button
|
||||
id="downloadButton"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate"
|
||||
[mat-dialog-close]="DownloadPromptActions.DOWNLOAD">
|
||||
{{ 'ADF_VIEWER.ACTIONS.DOWNLOAD' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
id="waitButton"
|
||||
color="primary"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.WAIT' | translate"
|
||||
[mat-dialog-close]="DownloadPromptActions.WAIT">
|
||||
{{ 'ADF_VIEWER.ACTIONS.WAIT' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
@@ -0,0 +1,64 @@
|
||||
/*!
|
||||
* @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 { MatDialogRef } from '@angular/material/dialog';
|
||||
import { CoreTestingModule, UnitTestingUtils } from '../../../testing';
|
||||
import { DownloadPromptActions } from '../../models/download-prompt.actions';
|
||||
import { DownloadPromptDialogComponent } from './download-prompt-dialog.component';
|
||||
|
||||
const mockDialog = {
|
||||
close: jasmine.createSpy('close')
|
||||
};
|
||||
|
||||
describe('DownloadPromptDialogComponent', () => {
|
||||
let matDialogRef: MatDialogRef<DownloadPromptDialogComponent>;
|
||||
let fixture: ComponentFixture<DownloadPromptDialogComponent>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
const clickButton = (buttonId: string) => testingUtils.clickByCSS(buttonId);
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule, DownloadPromptDialogComponent],
|
||||
providers: [{ provide: MatDialogRef, useValue: mockDialog }]
|
||||
});
|
||||
matDialogRef = TestBed.inject(MatDialogRef);
|
||||
|
||||
fixture = TestBed.createComponent(DownloadPromptDialogComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit DownloadPromptActions.WAIT and close dialog when clicking on the wait button', async () => {
|
||||
clickButton('#waitButton');
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(matDialogRef.close).toHaveBeenCalledWith(DownloadPromptActions.WAIT);
|
||||
});
|
||||
|
||||
it('should emit DownloadPromptActions.DOWNLOAD and close dialog when clicking on the download button', async () => {
|
||||
clickButton('#downloadButton');
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(matDialogRef.close).toHaveBeenCalledWith(DownloadPromptActions.DOWNLOAD);
|
||||
});
|
||||
});
|
@@ -0,0 +1,32 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DownloadPromptActions } from '../../models/download-prompt.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-download-prompt-dialog',
|
||||
standalone: true,
|
||||
imports: [MatDialogModule, TranslateModule, MatButtonModule],
|
||||
templateUrl: './download-prompt-dialog.component.html'
|
||||
})
|
||||
export class DownloadPromptDialogComponent {
|
||||
DownloadPromptActions = DownloadPromptActions;
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
<div id="adf-image-container"
|
||||
(keydown)="onKeyDown($event)"
|
||||
class="adf-image-container"
|
||||
tabindex="0"
|
||||
role="img"
|
||||
[attr.aria-label]="fileName"
|
||||
data-automation-id="adf-image-container">
|
||||
<img #image id="viewer-image"
|
||||
[src]="urlFile"
|
||||
[alt]="fileName"
|
||||
(load)="imageLoaded.emit()"
|
||||
(error)="onImageError()" />
|
||||
</div>
|
||||
|
||||
<div class="adf-image-viewer__toolbar" *ngIf="showToolbar">
|
||||
<adf-toolbar class="adf-main-toolbar">
|
||||
<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>zoom_out</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="adf-image-viewer__toolbar-page-scale" data-automation-id="adf-page-scale">
|
||||
{{ currentScaleText }}
|
||||
</div>
|
||||
|
||||
<button id="viewer-zoom-in-button"
|
||||
mat-icon-button
|
||||
title="{{ 'ADF_VIEWER.ARIA.ZOOM_IN' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ZOOM_IN' | translate }}"
|
||||
(click)="zoomIn()">
|
||||
<mat-icon>zoom_in</mat-icon>
|
||||
</button>
|
||||
|
||||
<button *ngIf="!readOnly && allowedEditActions.rotate" id="viewer-rotate-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.ROTATE' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ROTATE' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="rotateImage()">
|
||||
<mat-icon>rotate_left</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!readOnly && allowedEditActions.crop" id="viewer-crop-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.CROP' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.CROP' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="cropImage()">
|
||||
<mat-icon>crop</mat-icon>
|
||||
</button>
|
||||
|
||||
<button id="viewer-reset-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.RESET' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.RESET' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="reset()">
|
||||
<mat-icon>zoom_out_map</mat-icon>
|
||||
</button>
|
||||
|
||||
</adf-toolbar>
|
||||
|
||||
<adf-toolbar class="adf-secondary-toolbar" *ngIf="isEditing && !readOnly">
|
||||
<button id="viewer-cancel-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.CANCEL' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.CANCEL' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="reset()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
|
||||
<button id="viewer-save-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.SAVE' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.SAVE' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="save()">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
|
||||
</adf-toolbar>
|
||||
</div>
|
@@ -0,0 +1,66 @@
|
||||
@use 'mat-selectors' as ms;
|
||||
|
||||
.adf-image-viewer {
|
||||
width: 100%;
|
||||
|
||||
.adf-image-container {
|
||||
&:focus {
|
||||
outline-offset: -1px;
|
||||
outline: 1px solid var(--theme-accent-color-a200);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
height: 90vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* query for Microsoft IE 11 */
|
||||
@media screen and (-ms-high-contrast: active), screen and (-ms-high-contrast: none) {
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.adf-toolbar #{ms.$mat-toolbar} {
|
||||
max-height: 48px;
|
||||
background-color: var(--adf-theme-background-card-color);
|
||||
border-width: 0;
|
||||
border-radius: 2px;
|
||||
/* stylelint-disable-next-line */
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.adf-main-toolbar {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.adf-secondary-toolbar {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&-page-scale {
|
||||
cursor: default;
|
||||
width: 79px;
|
||||
height: 24px;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
border: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,384 @@
|
||||
/*!
|
||||
* @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 { SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { AppConfigService } from '../../../app-config';
|
||||
import { UrlService } from '../../../common';
|
||||
import { CoreTestingModule, UnitTestingUtils } from '../../../testing';
|
||||
import { ImgViewerComponent } from './img-viewer.component';
|
||||
|
||||
describe('Test Img viewer component ', () => {
|
||||
let component: ImgViewerComponent;
|
||||
let urlService: UrlService;
|
||||
let fixture: ComponentFixture<ImgViewerComponent>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
const createFakeBlob = () => {
|
||||
const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
|
||||
return new Blob([data], { type: 'image/png' });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule]
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zoom customization', () => {
|
||||
beforeEach(() => {
|
||||
urlService = TestBed.inject(UrlService);
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
component.urlFile = 'fake-url-file.png';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('default value', () => {
|
||||
it('should use default zoom if is not present a custom zoom in the app.config', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.scale).toBe(1.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom value', () => {
|
||||
beforeEach(() => {
|
||||
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
|
||||
appConfig.config['adf-viewer-render.image-viewer-scaling'] = 70;
|
||||
component.initializeScaling();
|
||||
});
|
||||
|
||||
it('should use the custom zoom if it is present in the app.config', (done) => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.scale).toBe(0.7);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url', () => {
|
||||
beforeEach(() => {
|
||||
urlService = TestBed.inject(UrlService);
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
component.urlFile =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
component.ngAfterViewInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display current scale as percent string', () => {
|
||||
component.scale = 0.5;
|
||||
expect(component.currentScaleText).toBe('50%');
|
||||
|
||||
component.scale = 1.0;
|
||||
expect(component.currentScaleText).toBe('100%');
|
||||
});
|
||||
|
||||
it('should define cropper after init', () => {
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
expect(component.cropper).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Blob', () => {
|
||||
beforeEach(() => {
|
||||
urlService = TestBed.inject(UrlService);
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
testingUtils.setDebugElement(fixture.debugElement);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should thrown an error if no url or blob are passed', () => {
|
||||
const change = new SimpleChange(null, null, true);
|
||||
expect(() => {
|
||||
component.ngOnChanges({ blobFile: change, urlFile: change });
|
||||
}).toThrow(new Error('Attribute urlFile or blobFile is required'));
|
||||
});
|
||||
|
||||
it('should not thrown an error if url is passed ', () => {
|
||||
component.urlFile = 'fake-url';
|
||||
expect(() => {
|
||||
component.ngOnChanges(null);
|
||||
}).not.toThrow(new Error('Attribute urlFile or blobFile is required'));
|
||||
});
|
||||
|
||||
it('should present file name in the alt attribute', () => {
|
||||
component.fileName = 'fake-name';
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('#viewer-image').nativeElement.getAttribute('alt')).toEqual('fake-name');
|
||||
});
|
||||
|
||||
it('should call replace on cropper with new url if blobFile is null', () => {
|
||||
component.urlFile = 'fake-url';
|
||||
spyOn(component.cropper, 'replace').and.stub();
|
||||
const urlFile = new SimpleChange('fake-url', 'fake-url-2', false);
|
||||
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ urlFile });
|
||||
|
||||
expect(component.cropper.replace).toHaveBeenCalledWith('fake-url-2');
|
||||
});
|
||||
|
||||
it('should not thrown an error if blob is passed ', () => {
|
||||
const blob = createFakeBlob();
|
||||
|
||||
spyOn(urlService, 'createTrustedUrl').and.returnValue('fake-blob-url');
|
||||
const change = new SimpleChange(null, blob, true);
|
||||
expect(() => {
|
||||
component.ngOnChanges({ blobFile: change });
|
||||
}).not.toThrow(new Error('Attribute urlFile or blobFile is required'));
|
||||
expect(component.urlFile).toEqual('fake-blob-url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toolbar actions', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
component.blobFile = createFakeBlob();
|
||||
const change = new SimpleChange(null, component.blobFile, true);
|
||||
component.ngOnChanges({ blobFile: change });
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should update scales on zoom in', fakeAsync(() => {
|
||||
spyOn(component, 'zoomIn').and.callThrough();
|
||||
spyOn(component.cropper, 'zoom');
|
||||
component.scale = 1.0;
|
||||
tick();
|
||||
|
||||
component.zoomIn();
|
||||
expect(component.scale).toBe(1.2);
|
||||
expect(component.cropper.zoom).toHaveBeenCalledWith(0.2);
|
||||
|
||||
component.zoomIn();
|
||||
expect(component.scale).toBe(1.4);
|
||||
expect(component.cropper.zoom).toHaveBeenCalledWith(0.2);
|
||||
}));
|
||||
|
||||
it('should update scales on zoom out', fakeAsync(() => {
|
||||
spyOn(component, 'zoomOut').and.callThrough();
|
||||
spyOn(component.cropper, 'zoom');
|
||||
component.scale = 1.0;
|
||||
tick();
|
||||
|
||||
component.zoomOut();
|
||||
expect(component.scale).toBe(0.8);
|
||||
expect(component.cropper.zoom).toHaveBeenCalledWith(-0.2);
|
||||
|
||||
component.zoomOut();
|
||||
expect(component.scale).toBe(0.6);
|
||||
expect(component.cropper.zoom).toHaveBeenCalledWith(-0.2);
|
||||
}));
|
||||
|
||||
it('should not zoom out past 20%', fakeAsync(() => {
|
||||
component.scale = 0.2;
|
||||
tick();
|
||||
|
||||
component.zoomOut();
|
||||
component.zoomOut();
|
||||
component.zoomOut();
|
||||
|
||||
expect(component.scale).toBe(0.2);
|
||||
}));
|
||||
|
||||
it('should show rotate button if not in read only mode', () => {
|
||||
component.readOnly = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('#viewer-rotate-button')).not.toEqual(null);
|
||||
});
|
||||
|
||||
it('should not show rotate button by default', () => {
|
||||
expect(testingUtils.getByCSS('#viewer-rotate-button')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not show crop button by default', () => {
|
||||
expect(testingUtils.getByCSS('#viewer-crop-button')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should start cropping when clicking the crop button', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
spyOn(component, 'cropImage').and.callThrough();
|
||||
spyOn(component.cropper, 'crop');
|
||||
spyOn(component.cropper, 'setDragMode');
|
||||
fixture.detectChanges();
|
||||
testingUtils.clickByCSS('#viewer-crop-button');
|
||||
tick();
|
||||
|
||||
expect(component.cropImage).toHaveBeenCalled();
|
||||
expect(component.cropper.crop).toHaveBeenCalled();
|
||||
expect(component.cropper.setDragMode).toHaveBeenCalledWith('crop');
|
||||
}));
|
||||
|
||||
it('should rotate image by -90 degrees on button click', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
spyOn(component, 'rotateImage').and.callThrough();
|
||||
spyOn(component.cropper, 'rotate');
|
||||
fixture.detectChanges();
|
||||
testingUtils.clickByCSS('#viewer-rotate-button');
|
||||
tick();
|
||||
|
||||
expect(component.rotateImage).toHaveBeenCalled();
|
||||
expect(component.cropper.rotate).toHaveBeenCalledWith(-90);
|
||||
}));
|
||||
|
||||
it('should display the second toolbar when in editing and not in read only mode', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
component.isEditing = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-secondary-toolbar')).not.toEqual(null);
|
||||
}));
|
||||
|
||||
it('should not display the second toolbar when in read only mode', () => {
|
||||
component.readOnly = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-secondary-toolbar')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not display the second toolbar when not in editing', () => {
|
||||
component.readOnly = true;
|
||||
component.isEditing = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-secondary-toolbar')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should display second toolbar in edit mode', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
component.isEditing = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-secondary-toolbar')).not.toEqual(null);
|
||||
expect(testingUtils.getByCSS('#viewer-cancel-button')).not.toEqual(null);
|
||||
expect(testingUtils.getByCSS('#viewer-save-button')).not.toEqual(null);
|
||||
}));
|
||||
|
||||
it('should not be in editing mode by default', () => {
|
||||
component.readOnly = false;
|
||||
|
||||
expect(component.isEditing).toEqual(false);
|
||||
});
|
||||
|
||||
it('should get in editing mode when the image gets rotated', () => {
|
||||
component.readOnly = false;
|
||||
component.rotateImage();
|
||||
|
||||
expect(component.isEditing).toEqual(true);
|
||||
});
|
||||
|
||||
it('should get in editing mode when the image gets cropped', () => {
|
||||
component.readOnly = false;
|
||||
component.cropImage();
|
||||
|
||||
expect(component.isEditing).toEqual(true);
|
||||
});
|
||||
|
||||
it('should reset the scale and hide second toolbar', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
component.isEditing = true;
|
||||
|
||||
spyOn(component, 'reset').and.callThrough();
|
||||
spyOn(component, 'updateCanvasContainer');
|
||||
spyOn(component.cropper, 'reset');
|
||||
spyOn(component.cropper, 'clear');
|
||||
spyOn(component.cropper, 'zoomTo');
|
||||
|
||||
fixture.detectChanges();
|
||||
testingUtils.clickByCSS('#viewer-cancel-button');
|
||||
tick();
|
||||
|
||||
expect(component.reset).toHaveBeenCalled();
|
||||
expect(component.scale).toEqual(1.0);
|
||||
expect(component.isEditing).toEqual(false);
|
||||
expect(component.cropper.reset).toHaveBeenCalled();
|
||||
expect(component.cropper.clear).toHaveBeenCalled();
|
||||
expect(component.updateCanvasContainer).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should save when clicked on toolbar button', fakeAsync(() => {
|
||||
component.readOnly = false;
|
||||
component.isEditing = true;
|
||||
|
||||
const canvasMock = document.createElement('canvas');
|
||||
spyOn(component.isSaving, 'emit');
|
||||
spyOn(component, 'save').and.callThrough();
|
||||
spyOn(component.cropper, 'getCroppedCanvas').and.returnValue(canvasMock);
|
||||
spyOn(component.cropper.getCroppedCanvas(), 'toBlob').and.callFake(() => component.isSaving.emit(false));
|
||||
|
||||
fixture.detectChanges();
|
||||
testingUtils.clickByCSS('#viewer-save-button');
|
||||
tick();
|
||||
|
||||
expect(component.save).toHaveBeenCalled();
|
||||
expect(component.isSaving.emit).toHaveBeenCalledWith(true);
|
||||
expect(component.isSaving.emit).toHaveBeenCalledWith(false);
|
||||
}));
|
||||
|
||||
it('should reset the viewer after going to full screen mode', () => {
|
||||
Object.defineProperty(document, 'fullscreenElement', {
|
||||
value: true
|
||||
});
|
||||
spyOn(component, 'reset');
|
||||
|
||||
document.dispatchEvent(new Event('fullscreenchange'));
|
||||
|
||||
expect(component.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowedEditActions', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
testingUtils.setDebugElement(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should conditionally display rotate and crop buttons based on allowedEditActions', () => {
|
||||
component.readOnly = false;
|
||||
component.allowedEditActions = { rotate: true, crop: true };
|
||||
fixture.detectChanges();
|
||||
|
||||
// Check both buttons are visible when allowed
|
||||
expect(testingUtils.getByCSS('#viewer-rotate-button')).not.toBeNull('Rotate button should be visible when allowed');
|
||||
expect(testingUtils.getByCSS('#viewer-crop-button')).not.toBeNull('Crop button should be visible when allowed');
|
||||
|
||||
// Change allowedEditActions to disallow both actions
|
||||
component.allowedEditActions = { rotate: false, crop: false };
|
||||
fixture.detectChanges();
|
||||
|
||||
// Check both buttons are not visible when not allowed
|
||||
expect(testingUtils.getByCSS('#viewer-rotate-button')).toBeNull('Rotate button should not be visible when disallowed');
|
||||
expect(testingUtils.getByCSS('#viewer-crop-button')).toBeNull('Crop button should not be visible when disallowed');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,247 @@
|
||||
/*!
|
||||
* @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 {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import Cropper from 'cropperjs';
|
||||
import { AppConfigService, UrlService, ToolbarComponent } from '@alfresco/adf-core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-img-viewer',
|
||||
standalone: true,
|
||||
templateUrl: './img-viewer.component.html',
|
||||
styleUrls: ['./img-viewer.component.scss'],
|
||||
host: { class: 'adf-image-viewer' },
|
||||
imports: [ToolbarComponent, TranslateModule, MatIconModule, MatButtonModule, NgIf],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
showToolbar = true;
|
||||
|
||||
@Input()
|
||||
readOnly = true;
|
||||
|
||||
@Input()
|
||||
allowedEditActions: { [key: string]: boolean } = {
|
||||
rotate: true,
|
||||
crop: true
|
||||
};
|
||||
|
||||
@Input()
|
||||
urlFile: string;
|
||||
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
@Input()
|
||||
fileName: string;
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/no-output-native
|
||||
@Output()
|
||||
error = new EventEmitter<any>();
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/no-output-native
|
||||
@Output()
|
||||
submit = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
isSaving = new EventEmitter<boolean>();
|
||||
|
||||
@Output()
|
||||
imageLoaded = new EventEmitter<void>();
|
||||
|
||||
@ViewChild('image', { static: false })
|
||||
imageElement: ElementRef;
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
this.cropper.move(-3, 0);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
this.cropper.move(0, -3);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
this.cropper.move(3, 0);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
this.cropper.move(0, 3);
|
||||
break;
|
||||
case 'i':
|
||||
this.zoomIn();
|
||||
break;
|
||||
case 'o':
|
||||
this.zoomOut();
|
||||
break;
|
||||
case 'r':
|
||||
this.rotateImage();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:fullscreenchange')
|
||||
fullScreenChangeHandler() {
|
||||
if (document.fullscreenElement) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
scale: number = 1.0;
|
||||
cropper: Cropper;
|
||||
isEditing: boolean = false;
|
||||
|
||||
get currentScaleText(): string {
|
||||
return Math.round(this.scale * 100) + '%';
|
||||
}
|
||||
|
||||
constructor(private appConfigService: AppConfigService, private urlService: UrlService) {
|
||||
this.initializeScaling();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const blobFile = changes['blobFile'];
|
||||
if (blobFile?.currentValue) {
|
||||
this.urlFile = this.urlService.createTrustedUrl(this.blobFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changes['urlFile'].firstChange) {
|
||||
if (changes['urlFile'].previousValue !== changes['urlFile'].currentValue) {
|
||||
this.cropper.replace(changes['urlFile'].currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.urlFile && !this.blobFile) {
|
||||
throw new Error('Attribute urlFile or blobFile is required');
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.cropper = new Cropper(this.imageElement.nativeElement, {
|
||||
autoCrop: false,
|
||||
checkOrientation: false,
|
||||
dragMode: 'move',
|
||||
background: false,
|
||||
scalable: true,
|
||||
zoomOnWheel: true,
|
||||
toggleDragModeOnDblclick: false,
|
||||
viewMode: 1,
|
||||
checkCrossOrigin: false,
|
||||
ready: () => {
|
||||
this.updateCanvasContainer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.cropper.destroy();
|
||||
}
|
||||
|
||||
initializeScaling() {
|
||||
const scaling = this.appConfigService.get<number>('adf-viewer-render.image-viewer-scaling', undefined) / 100;
|
||||
if (scaling) {
|
||||
this.scale = scaling;
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
this.cropper.zoom(0.2);
|
||||
this.scale = +(this.scale + 0.2).toFixed(1);
|
||||
}
|
||||
|
||||
zoomOut() {
|
||||
if (this.scale > 0.2) {
|
||||
this.cropper.zoom(-0.2);
|
||||
this.scale = +(this.scale - 0.2).toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
rotateImage() {
|
||||
this.isEditing = true;
|
||||
this.cropper.rotate(-90);
|
||||
}
|
||||
|
||||
cropImage() {
|
||||
this.isEditing = true;
|
||||
this.cropper.setDragMode('crop');
|
||||
this.cropper.crop();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.isSaving.emit(true);
|
||||
this.isEditing = false;
|
||||
this.cropper.setDragMode('move');
|
||||
this.cropper.getCroppedCanvas().toBlob((blob) => {
|
||||
this.submit.emit(blob);
|
||||
this.cropper.replace(this.cropper.getCroppedCanvas().toDataURL());
|
||||
this.cropper.clear();
|
||||
this.isSaving.emit(false);
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.isEditing = false;
|
||||
this.cropper.clear();
|
||||
this.cropper.reset();
|
||||
this.cropper.setDragMode('move');
|
||||
this.scale = 1.0;
|
||||
this.updateCanvasContainer();
|
||||
}
|
||||
|
||||
updateCanvasContainer() {
|
||||
if (this.imageElement.nativeElement.width < this.cropper.getContainerData().width) {
|
||||
const width = this.imageElement.nativeElement.width;
|
||||
const height = this.imageElement.nativeElement.height;
|
||||
const top = (this.cropper.getContainerData().height - this.imageElement.nativeElement.height) / 2;
|
||||
const left = (this.cropper.getContainerData().width - this.imageElement.nativeElement.width) / 2;
|
||||
|
||||
this.cropper.setCanvasData({
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onImageError() {
|
||||
this.error.emit();
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<video controls class="adf-video-player" (canplay)="canPlay.emit()"
|
||||
[ngClass]="{ 'adf-audio-file': mimeType && mimeType.startsWith('audio') }">
|
||||
<source [src]="urlFile"
|
||||
[type]="mimeType"
|
||||
(error)="onMediaPlayerError($event)" />
|
||||
<track *ngFor="let track of tracks"
|
||||
[kind]="track.kind"
|
||||
[label]="track.label"
|
||||
[srclang]="track.srclang"
|
||||
[src]="track.src" />
|
||||
</video>
|
@@ -0,0 +1,14 @@
|
||||
.adf-media-player {
|
||||
display: flex;
|
||||
|
||||
video {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
max-height: 90vh;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
video.adf-video-player.adf-audio-file::-webkit-media-text-track-container {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
/*!
|
||||
* @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 { NgClass, NgForOf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { UrlService } from '@alfresco/adf-core';
|
||||
import { Track } from '../../models/viewer.model';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-media-player',
|
||||
standalone: true,
|
||||
templateUrl: './media-player.component.html',
|
||||
styleUrls: ['./media-player.component.scss'],
|
||||
host: { class: 'adf-media-player' },
|
||||
imports: [NgClass, NgForOf],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MediaPlayerComponent implements OnChanges {
|
||||
@Input()
|
||||
urlFile: string;
|
||||
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
@Input()
|
||||
mimeType: string;
|
||||
|
||||
@Input()
|
||||
fileName: string;
|
||||
|
||||
/** media subtitles for the media player*/
|
||||
@Input()
|
||||
tracks: Track[] = [];
|
||||
|
||||
@Output()
|
||||
error = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
canPlay = new EventEmitter<void>();
|
||||
|
||||
constructor(private urlService: UrlService) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const blobFile = changes['blobFile'];
|
||||
|
||||
if (blobFile?.currentValue) {
|
||||
this.urlFile = this.urlService.createTrustedUrl(this.blobFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.urlFile && !this.blobFile) {
|
||||
throw new Error('Attribute urlFile or blobFile is required');
|
||||
}
|
||||
}
|
||||
|
||||
onMediaPlayerError(event: any) {
|
||||
this.error.emit(event);
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { ViewerComponent } from '../viewer.component';
|
||||
import { ViewerMoreActionsComponent } from '../viewer-more-actions.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-container-more-actions',
|
||||
standalone: true,
|
||||
imports: [ViewerComponent, MatIconModule, MatMenuModule, ViewerMoreActionsComponent],
|
||||
template: `
|
||||
<adf-viewer>
|
||||
<adf-viewer-more-actions>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>dialpad</mat-icon>
|
||||
<span>Action One</span>
|
||||
</button>
|
||||
<button mat-menu-item [disabled]="true">
|
||||
<mat-icon>voicemail</mat-icon>
|
||||
<span>Action Two</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>notifications_off</mat-icon>
|
||||
<span>Action Three</span>
|
||||
</button>
|
||||
</adf-viewer-more-actions>
|
||||
</adf-viewer>
|
||||
`
|
||||
})
|
||||
export class ViewerWithCustomMoreActionsComponent {}
|
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { ViewerComponent } from '../viewer.component';
|
||||
import { ViewerOpenWithComponent } from '../viewer-open-with.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-container-open-with',
|
||||
standalone: true,
|
||||
imports: [ViewerComponent, MatIconModule, MatMenuModule, ViewerOpenWithComponent],
|
||||
template: `
|
||||
<adf-viewer>
|
||||
<adf-viewer-open-with>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>dialpad</mat-icon>
|
||||
<span>Option 1</span>
|
||||
</button>
|
||||
<button mat-menu-item [disabled]="true">
|
||||
<mat-icon>voicemail</mat-icon>
|
||||
<span>Option 2</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>notifications_off</mat-icon>
|
||||
<span>Option 3</span>
|
||||
</button>
|
||||
</adf-viewer-open-with>
|
||||
</adf-viewer>
|
||||
`
|
||||
})
|
||||
export class ViewerWithCustomOpenWithComponent {}
|
@@ -0,0 +1,34 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { ViewerComponent } from '../viewer.component';
|
||||
import { ViewerSidebarComponent } from '../viewer-sidebar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-container-sidebar',
|
||||
standalone: true,
|
||||
imports: [ViewerComponent, ViewerSidebarComponent],
|
||||
template: `
|
||||
<adf-viewer>
|
||||
<adf-viewer-sidebar>
|
||||
<div class="custom-sidebar"></div>
|
||||
</adf-viewer-sidebar>
|
||||
</adf-viewer>
|
||||
`
|
||||
})
|
||||
export class ViewerWithCustomSidebarComponent {}
|
@@ -0,0 +1,38 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ViewerComponent } from '../viewer.component';
|
||||
import { ViewerToolbarActionsComponent } from '../viewer-toolbar-actions.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-container-toolbar-actions',
|
||||
standalone: true,
|
||||
imports: [ViewerComponent, MatIconModule, MatButtonModule, ViewerToolbarActionsComponent],
|
||||
template: `
|
||||
<adf-viewer>
|
||||
<adf-viewer-toolbar-actions>
|
||||
<button mat-icon-button id="custom-button">
|
||||
<mat-icon>alarm</mat-icon>
|
||||
</button>
|
||||
</adf-viewer-toolbar-actions>
|
||||
</adf-viewer>
|
||||
`
|
||||
})
|
||||
export class ViewerWithCustomToolbarActionsComponent {}
|
@@ -0,0 +1,34 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { ViewerComponent } from '../viewer.component';
|
||||
import { ViewerToolbarComponent } from '../viewer-toolbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-container-toolbar',
|
||||
standalone: true,
|
||||
imports: [ViewerComponent, ViewerToolbarComponent],
|
||||
template: `
|
||||
<adf-viewer>
|
||||
<adf-viewer-toolbar>
|
||||
<div class="custom-toolbar-element"></div>
|
||||
</adf-viewer-toolbar>
|
||||
</adf-viewer>
|
||||
`
|
||||
})
|
||||
export class ViewerWithCustomToolbarComponent {}
|
37
lib/core/viewer/src/components/mock/pdfjs-lib.mock.ts
Normal file
37
lib/core/viewer/src/components/mock/pdfjs-lib.mock.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*!
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export default {
|
||||
GlobalWorkerOptions: {},
|
||||
getDocument() {
|
||||
return {
|
||||
loadingTask: () => ({
|
||||
destroy: () => Promise.resolve()
|
||||
}),
|
||||
promise: new Promise((resolve) => {
|
||||
resolve({
|
||||
numPages: 6,
|
||||
getPage: () => 'fakePage'
|
||||
});
|
||||
})
|
||||
};
|
||||
},
|
||||
PasswordResponses: {
|
||||
NEED_PASSWORD: 1,
|
||||
INCORRECT_PASSWORD: 2
|
||||
}
|
||||
};
|
@@ -0,0 +1,31 @@
|
||||
<div mat-dialog-title>
|
||||
<mat-icon>lock</mat-icon>
|
||||
</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>
|
@@ -0,0 +1,15 @@
|
||||
.adf-fill-remaining-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.adf-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-dialog-buttons button {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.adf-dialog-action-button:enabled {
|
||||
color: var(--theme-primary-color);
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
/*!
|
||||
* @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 { CoreTestingModule } from '../../../testing';
|
||||
import { PdfPasswordDialogComponent } from './pdf-viewer-password-dialog';
|
||||
|
||||
declare const pdfjsLib: any;
|
||||
|
||||
describe('PdfPasswordDialogComponent', () => {
|
||||
let component: PdfPasswordDialogComponent;
|
||||
let fixture: ComponentFixture<PdfPasswordDialogComponent>;
|
||||
let dialogRef: MatDialogRef<PdfPasswordDialogComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule],
|
||||
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');
|
||||
});
|
||||
});
|
@@ -0,0 +1,58 @@
|
||||
/*!
|
||||
* @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, Inject, OnInit, ViewEncapsulation } 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 { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
declare const pdfjsLib: any;
|
||||
|
||||
@Component({
|
||||
selector: 'adf-pdf-viewer-password-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './pdf-viewer-password-dialog.html',
|
||||
styleUrls: ['./pdf-viewer-password-dialog.scss'],
|
||||
imports: [MatDialogModule, MatIconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslateModule, NgIf, MatButtonModule],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class PdfPasswordDialogComponent implements OnInit {
|
||||
passwordFormControl: UntypedFormControl;
|
||||
|
||||
constructor(private dialogRef: MatDialogRef<PdfPasswordDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
<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>
|
@@ -0,0 +1,74 @@
|
||||
/*!
|
||||
* @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';
|
||||
import { CoreTestingModule } from '../../../testing';
|
||||
|
||||
describe('PdfThumbComponent', () => {
|
||||
let fixture: ComponentFixture<PdfThumbComponent>;
|
||||
let component: PdfThumbComponent;
|
||||
const domSanitizer = {
|
||||
bypassSecurityTrustUrl: () => 'image-data'
|
||||
};
|
||||
const width = 91;
|
||||
const height = 119;
|
||||
const page = {
|
||||
id: 'pageId',
|
||||
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: [CoreTestingModule],
|
||||
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);
|
||||
});
|
||||
});
|
@@ -0,0 +1,71 @@
|
||||
/*!
|
||||
* @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 } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-pdf-thumb',
|
||||
standalone: true,
|
||||
templateUrl: './pdf-viewer-thumb.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [AsyncPipe, TranslateModule, NgIf],
|
||||
host: { tabindex: '0' }
|
||||
})
|
||||
export class PdfThumbComponent implements OnInit, FocusableOption {
|
||||
@Input()
|
||||
page: any = null;
|
||||
|
||||
image$: Promise<string>;
|
||||
|
||||
constructor(private sanitizer: DomSanitizer, private element: ElementRef) {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div class="adf-pdf-thumbnails__content"
|
||||
data-automation-id='adf-thumbnails-content'
|
||||
[style.height.px]="virtualHeight"
|
||||
[style.transform]="'translate(-50%, ' + translateY + 'px)'">
|
||||
<adf-pdf-thumb *ngFor="let page of renderItems; trackBy: trackByFn"
|
||||
class="adf-pdf-thumbnails__thumb"
|
||||
[id]="page.id"
|
||||
[ngClass]="{'adf-pdf-thumbnails__thumb--selected' : isSelected(page.id)}"
|
||||
[page]="page"
|
||||
(click)="goTo(page.id)" />
|
||||
</div>
|
@@ -0,0 +1,30 @@
|
||||
.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(--theme-background-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__thumb:hover {
|
||||
box-shadow: 0 0 5px 0 var(--adf-theme-foreground-text-color-087);
|
||||
}
|
||||
|
||||
&__thumb--selected {
|
||||
border: 2px solid var(--theme-accent-color-a200);
|
||||
}
|
||||
}
|
@@ -0,0 +1,236 @@
|
||||
/*!
|
||||
* @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 { CoreTestingModule, 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: [CoreTestingModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(PdfThumbListComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
component.pdfViewer = viewerMock;
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,249 @@
|
||||
/*!
|
||||
* @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, NgForOf } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
QueryList,
|
||||
TemplateRef,
|
||||
ViewChildren,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { PdfThumbComponent } from '../pdf-viewer-thumb/pdf-viewer-thumb.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-pdf-thumbnails',
|
||||
standalone: true,
|
||||
templateUrl: './pdf-viewer-thumbnails.component.html',
|
||||
styleUrls: ['./pdf-viewer-thumbnails.component.scss'],
|
||||
host: { class: 'adf-pdf-thumbnails' },
|
||||
imports: [PdfThumbComponent, NgClass, NgForOf],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class PdfThumbListComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input({ required: true }) pdfViewer: any;
|
||||
|
||||
@Output()
|
||||
close = new EventEmitter<void>();
|
||||
|
||||
virtualHeight: number = 0;
|
||||
translateY: number = 0;
|
||||
renderItems = [];
|
||||
width: number = 91;
|
||||
currentHeight: number = 0;
|
||||
|
||||
private items = [];
|
||||
private margin: number = 15;
|
||||
private itemHeight: number = 114 + this.margin;
|
||||
private previouslyFocusedElement: HTMLElement | null = null;
|
||||
private keyManager: FocusKeyManager<PdfThumbComponent>;
|
||||
|
||||
@ContentChild(TemplateRef)
|
||||
template: any;
|
||||
|
||||
@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(private element: ElementRef, @Inject(DOCUMENT) private document: any) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
trackByFn(_: number, item: any): number {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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 {
|
||||
const height = this.pdfViewer.pdfDocument.getPage(id).then((page) => this.calculateHeight(page));
|
||||
return height;
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,208 @@
|
||||
.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-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);
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
<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>close</mat-icon>
|
||||
</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 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>dashboard</mat-icon>
|
||||
</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>keyboard_arrow_up</mat-icon>
|
||||
</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>keyboard_arrow_down</mat-icon>
|
||||
</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>zoom_in</mat-icon>
|
||||
</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>zoom_out</mat-icon>
|
||||
</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>zoom_out_map</mat-icon>
|
||||
</button>
|
||||
|
||||
</adf-toolbar>
|
||||
</div>
|
@@ -0,0 +1,143 @@
|
||||
@use '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: rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
.adf-info-drawer-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--adf-theme-foreground-text-color-054);
|
||||
}
|
||||
}
|
||||
|
||||
&__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(--adf-theme-background-card-color);
|
||||
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;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
|
||||
& > input {
|
||||
border: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||
background-color: var(--adf-theme-background-card-color);
|
||||
color: inherit;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
padding: 5px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: right;
|
||||
width: 33px;
|
||||
margin-right: 4px;
|
||||
outline-width: 1px;
|
||||
outline-color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
&-page-scale {
|
||||
cursor: default;
|
||||
width: 79px;
|
||||
height: 24px;
|
||||
font-size: var(--theme-body-1-font-size);
|
||||
border: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,603 @@
|
||||
/*!
|
||||
* @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, 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 { NoopAuthModule, NoopTranslateModule, UnitTestingUtils } 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 pdfjsLibMock from '../mock/pdfjs-lib.mock';
|
||||
|
||||
declare const pdfjsLib: any;
|
||||
|
||||
@Component({
|
||||
selector: 'adf-url-test-component',
|
||||
standalone: true,
|
||||
imports: [PdfViewerComponent],
|
||||
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [urlFile]="urlFile" /> `
|
||||
})
|
||||
class UrlTestComponent {
|
||||
@ViewChild(PdfViewerComponent, { static: true })
|
||||
pdfViewerComponent: PdfViewerComponent;
|
||||
|
||||
urlFile: any;
|
||||
|
||||
constructor() {
|
||||
this.urlFile = './fake-test-file.pdf';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-url-test-password-component',
|
||||
standalone: true,
|
||||
imports: [PdfViewerComponent],
|
||||
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [urlFile]="urlFile" /> `
|
||||
})
|
||||
class UrlTestPasswordComponent {
|
||||
@ViewChild(PdfViewerComponent, { static: true })
|
||||
pdfViewerComponent: PdfViewerComponent;
|
||||
|
||||
urlFile: any;
|
||||
|
||||
constructor() {
|
||||
this.urlFile = './fake-test-password-file.pdf';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [PdfViewerComponent],
|
||||
template: ` <adf-pdf-viewer [allowThumbnails]="true" [showToolbar]="true" [blobFile]="blobFile" /> `
|
||||
})
|
||||
class BlobTestComponent {
|
||||
@ViewChild(PdfViewerComponent, { static: true })
|
||||
pdfViewerComponent: PdfViewerComponent;
|
||||
|
||||
blobFile: any;
|
||||
|
||||
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: any;
|
||||
let dialog: MatDialog;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAuthModule, NoopTranslateModule, PdfViewerComponent],
|
||||
providers: [
|
||||
{
|
||||
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((_: any, context: any) => {
|
||||
if (context.data.reason === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
|
||||
return {
|
||||
afterClosed: () => of('wrong_password')
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (context.data.reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
|
||||
return {
|
||||
afterClosed: () => of('password')
|
||||
} as any;
|
||||
}
|
||||
|
||||
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 any)
|
||||
);
|
||||
|
||||
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: [NoopAuthModule, NoopTranslateModule, PdfViewerComponent],
|
||||
providers: [
|
||||
{
|
||||
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: [NoopAuthModule, NoopTranslateModule, PdfViewerComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MatDialog,
|
||||
useValue: {
|
||||
open: () => {}
|
||||
}
|
||||
},
|
||||
RenderingQueueServices,
|
||||
{ provide: PDFJS_VIEWER_MODULE, useValue: pdfViewerSpy },
|
||||
{ provide: PDFJS_MODULE, useValue: pdfjsLibMock }
|
||||
]
|
||||
});
|
||||
|
||||
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: { currentValue: './fake-test-file.pdf' } } as any);
|
||||
|
||||
flush();
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should init the viewer with annotation mode disabled', () => {
|
||||
expect(pdfViewerSpy).toHaveBeenCalledWith(jasmine.objectContaining({ annotationMode: 0 }));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,640 @@
|
||||
/*!
|
||||
* @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 { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { from, Subject, switchMap } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { AppConfigService, ToolbarComponent, ToolbarDividerComponent } from '@alfresco/adf-core';
|
||||
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 { EventBus, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs';
|
||||
import { OnProgressParameters, PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
|
||||
|
||||
export type PdfScaleMode = 'init' | 'page-actual' | 'page-width' | 'page-height' | 'page-fit' | 'auto';
|
||||
|
||||
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',
|
||||
standalone: true,
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
styleUrls: ['./pdf-viewer-host.component.scss', './pdf-viewer.component.scss'],
|
||||
providers: [RenderingQueueServices],
|
||||
host: { class: 'adf-pdf-viewer' },
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule,
|
||||
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<any> = null;
|
||||
|
||||
@Input()
|
||||
cacheType: string = '';
|
||||
|
||||
@Output()
|
||||
rendered = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
error = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
close = new EventEmitter<any>();
|
||||
|
||||
@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: any } = { 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 pdfjsLib = inject(PDFJS_MODULE);
|
||||
private pdfjsViewer = inject(PDFJS_VIEWER_MODULE);
|
||||
|
||||
private eventBus = new EventBus();
|
||||
private pdfjsDefaultOptions = {
|
||||
disableAutoFetch: true,
|
||||
disableStream: true,
|
||||
cMapUrl: './cmaps/',
|
||||
cMapPacked: true
|
||||
};
|
||||
private pdfjsWorkerDestroy$ = new Subject<boolean>();
|
||||
|
||||
private dialog = inject(MatDialog);
|
||||
private renderingQueueServices = inject(RenderingQueueServices);
|
||||
private appConfigService = inject(AppConfigService);
|
||||
|
||||
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: any = {
|
||||
...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: any) {
|
||||
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(() => 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: any = this.getViewer();
|
||||
const container = this.getDocumentContainer();
|
||||
|
||||
if (viewer && container) {
|
||||
this.pdfViewer = new this.pdfjsViewer({
|
||||
container,
|
||||
viewer,
|
||||
renderingQueue: this.renderingQueueServices,
|
||||
eventBus: this.eventBus,
|
||||
annotationMode: 0
|
||||
});
|
||||
|
||||
// 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.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(): HTMLElement {
|
||||
return document.getElementById(`${this.randomPdfId}-viewer-viewerPdf`);
|
||||
}
|
||||
|
||||
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: any = this.pdfViewer.currentScaleValue;
|
||||
do {
|
||||
newScale = (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: any = this.pdfViewer.currentScaleValue;
|
||||
do {
|
||||
newScale = (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 event
|
||||
*/
|
||||
onPageChange(event: any) {
|
||||
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.pagesLoaded.emit();
|
||||
this.isPanelDisabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
<pre class="adf-txt-viewer-content">
|
||||
{{content}}
|
||||
</pre>
|
@@ -0,0 +1,6 @@
|
||||
.adf-txt-viewer {
|
||||
background-color: var(--theme-background-color);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* @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 { SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TxtViewerComponent } from './txt-viewer.component';
|
||||
import { CoreTestingModule, UnitTestingUtils } from '@alfresco/adf-core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('Text View component', () => {
|
||||
let component: TxtViewerComponent;
|
||||
let fixture: ComponentFixture<TxtViewerComponent>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule, TxtViewerComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(TxtViewerComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
|
||||
const httpClient = TestBed.inject(HttpClient);
|
||||
spyOn(httpClient, 'get').and.returnValue(of('example'));
|
||||
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
it('Should text container be present with urlFile', async () => {
|
||||
fixture.detectChanges();
|
||||
const urlFile = './fake-test-file.txt';
|
||||
const change = new SimpleChange(null, urlFile, true);
|
||||
|
||||
await component.ngOnChanges({ urlFile: change });
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-txt-viewer-content').nativeElement.textContent).toContain('example');
|
||||
});
|
||||
|
||||
it('Should text container be present with Blob file', async () => {
|
||||
const blobFile = new Blob(['text example'], { type: 'text/txt' });
|
||||
|
||||
const change = new SimpleChange(null, blobFile, true);
|
||||
|
||||
await component.ngOnChanges({ blobFile: change });
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByCSS('.adf-txt-viewer-content').nativeElement.textContent).toContain('example');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,91 @@
|
||||
/*!
|
||||
* @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 { HttpClient } from '@angular/common/http';
|
||||
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-txt-viewer',
|
||||
standalone: true,
|
||||
templateUrl: './txt-viewer.component.html',
|
||||
styleUrls: ['./txt-viewer.component.scss'],
|
||||
host: { class: 'adf-txt-viewer' },
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class TxtViewerComponent implements OnChanges {
|
||||
@Input()
|
||||
urlFile: any;
|
||||
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
content: string | ArrayBuffer;
|
||||
|
||||
constructor(private http: HttpClient, private appConfigService: AppConfigService) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): Promise<void> {
|
||||
const blobFile = changes['blobFile'];
|
||||
if (blobFile?.currentValue) {
|
||||
return this.readBlob(blobFile.currentValue);
|
||||
}
|
||||
|
||||
const urlFile = changes['urlFile'];
|
||||
if (urlFile?.currentValue) {
|
||||
return this.getUrlContent(urlFile.currentValue);
|
||||
}
|
||||
|
||||
if (!this.urlFile && !this.blobFile) {
|
||||
throw new Error('Attribute urlFile or blobFile is required');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private getUrlContent(url: string): Promise<void> {
|
||||
const withCredentialsMode = this.appConfigService.get<boolean>('auth.withCredentials', false);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get(url, { responseType: 'text', withCredentials: withCredentialsMode }).subscribe(
|
||||
(res) => {
|
||||
this.content = res;
|
||||
resolve();
|
||||
},
|
||||
(event) => {
|
||||
reject(event);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private readBlob(blob: Blob): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
this.content = reader.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
reader.onerror = (error: any) => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsText(blob);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
<div class="adf-viewer__unknown-format-view">
|
||||
<div>
|
||||
<mat-icon class="icon">error</mat-icon>
|
||||
<div class="adf-viewer__unknown-label">{{ customError || 'ADF_VIEWER.UNKNOWN_FORMAT' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,9 @@
|
||||
.adf-viewer__unknown-format-view {
|
||||
height: 90vh;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: var(--adf-theme-foreground-text-color);
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
/*!
|
||||
* @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 { UnknownFormatComponent } from './unknown-format.component';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CoreTestingModule } from '@alfresco/adf-core';
|
||||
|
||||
describe('Unknown Format Component', () => {
|
||||
let fixture: ComponentFixture<UnknownFormatComponent>;
|
||||
|
||||
const getErrorMessageElement = (): string => fixture.debugElement.nativeElement.querySelector('.adf-viewer__unknown-label').innerText;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(UnknownFormatComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render default error message', () => {
|
||||
expect(getErrorMessageElement()).toBe('ADF_VIEWER.UNKNOWN_FORMAT');
|
||||
});
|
||||
|
||||
it('should render custom error message if such provided', () => {
|
||||
const errorMessage = 'Custom error message';
|
||||
fixture.componentInstance.customError = errorMessage;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getErrorMessageElement()).toBe(errorMessage);
|
||||
});
|
||||
});
|
@@ -0,0 +1,34 @@
|
||||
/*!
|
||||
* @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 { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-unknown-format',
|
||||
standalone: true,
|
||||
templateUrl: './unknown-format.component.html',
|
||||
styleUrls: ['./unknown-format.component.scss'],
|
||||
imports: [MatIconModule, TranslateModule],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class UnknownFormatComponent {
|
||||
/** Custom error message to be displayed . */
|
||||
@Input()
|
||||
customError: string;
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-more-actions',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-more-actions' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerMoreActionsComponent {}
|
28
lib/core/viewer/src/components/viewer-open-with.component.ts
Normal file
28
lib/core/viewer/src/components/viewer-open-with.component.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-open-with',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-open-with' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerOpenWithComponent {}
|
@@ -0,0 +1,105 @@
|
||||
<div *ngIf="isLoading$ | async" class="adf-viewer-render-main-loader">
|
||||
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
|
||||
<div class="adf-viewer-render-content-container">
|
||||
<div class="adf-viewer-render__loading-screen">
|
||||
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
|
||||
<div>
|
||||
<mat-spinner class="adf-viewer-render__loading-screen__spinner" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="urlFile || blobFile">
|
||||
<div [hidden]="isLoading$ | async" class="adf-viewer-render-main">
|
||||
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
|
||||
<div class="adf-viewer-render-content-container" [ngSwitch]="viewerType">
|
||||
<ng-container *ngSwitchCase="'external'">
|
||||
<adf-preview-extension
|
||||
*ngIf="!!externalViewer"
|
||||
[id]="externalViewer.component"
|
||||
[url]="urlFile"
|
||||
[extension]="externalViewer.fileExtension"
|
||||
[nodeId]="nodeId"
|
||||
[attr.data-automation-id]="externalViewer.component"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'pdf'">
|
||||
<adf-pdf-viewer
|
||||
[thumbnailsTemplate]="thumbnailsTemplate"
|
||||
[allowThumbnails]="allowThumbnails"
|
||||
[blobFile]="blobFile"
|
||||
[urlFile]="urlFile"
|
||||
[fileName]="internalFileName"
|
||||
[cacheType]="cacheTypeForContent"
|
||||
(pagesLoaded)="markAsLoaded()"
|
||||
(close)="onClose()"
|
||||
(error)="onUnsupportedFile()"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'image'">
|
||||
<adf-img-viewer
|
||||
[urlFile]="urlFile"
|
||||
[readOnly]="readOnly"
|
||||
[fileName]="internalFileName"
|
||||
[allowedEditActions]="allowedEditActions"
|
||||
[blobFile]="blobFile"
|
||||
(error)="onUnsupportedFile()"
|
||||
(submit)="onSubmitFile($event)"
|
||||
(imageLoaded)="markAsLoaded()"
|
||||
(isSaving)="isSaving.emit($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'media'">
|
||||
<adf-media-player
|
||||
id="adf-mdedia-player"
|
||||
[urlFile]="urlFile"
|
||||
[tracks]="tracks"
|
||||
[mimeType]="mimeType"
|
||||
[blobFile]="blobFile"
|
||||
[fileName]="internalFileName"
|
||||
(error)="onUnsupportedFile()"
|
||||
(canPlay)="markAsLoaded()"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'text'">
|
||||
<adf-txt-viewer [urlFile]="urlFile" [blobFile]="blobFile" />
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'custom'">
|
||||
<ng-container *ngFor="let ext of viewerExtensions">
|
||||
<adf-preview-extension
|
||||
*ngIf="checkExtensions(ext.fileExtension)"
|
||||
[id]="ext.component"
|
||||
[url]="urlFile"
|
||||
[extension]="extension"
|
||||
[nodeId]="nodeId"
|
||||
[attr.data-automation-id]="ext.component"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let extensionTemplate of extensionTemplates">
|
||||
<span *ngIf="extensionTemplate.isVisible" class="adf-viewer-render-custom-content">
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="extensionTemplate.template"
|
||||
[ngTemplateOutletContext]="{ urlFile: urlFile, extension: extension }"
|
||||
/>
|
||||
</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<adf-viewer-unknown-format [customError]="customError" />
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="viewerTemplateExtensions">
|
||||
<ng-template [ngTemplateOutlet]="viewerTemplateExtensions" [ngTemplateOutletInjector]="injector" />
|
||||
</ng-container>
|
@@ -0,0 +1,88 @@
|
||||
/* stylelint-disable scss/at-extend-no-missing-placeholder */
|
||||
|
||||
.adf-full-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--adf-theme-background-card-color);
|
||||
}
|
||||
|
||||
.adf-viewer-render-main-loader {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.adf-viewer-render {
|
||||
&-main {
|
||||
width: 0;
|
||||
order: 1;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&-content-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-layout-content {
|
||||
@extend .adf-full-screen;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
background-color: var(--theme-background-color);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex: 1;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
height: 93vh;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-overlay-container {
|
||||
.adf-viewer-render-content {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading-screen {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
|
||||
&__spinner {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-custom-content {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
&-unknown-content {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-pdf {
|
||||
display: contents;
|
||||
}
|
||||
}
|
@@ -0,0 +1,540 @@
|
||||
/*!
|
||||
* @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 { 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 { ComponentFixture, DeferBlockBehavior, TestBed } from '@angular/core/testing';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { NoopTranslateModule, 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-double-viewer',
|
||||
standalone: true,
|
||||
imports: [ViewerExtensionDirective, ViewerRenderComponent],
|
||||
template: `
|
||||
<adf-viewer-render [urlFile]="urlFileViewer1" [viewerTemplateExtensions]="viewerTemplateExtensions" #viewer1 />
|
||||
<adf-viewer-render [urlFile]="urlFileViewer2" #viewer2 />
|
||||
<ng-template #viewerExtension>
|
||||
<adf-viewer-extension [supportedExtensions]="['json']">
|
||||
<ng-template>
|
||||
<h1>JSON Viewer</h1>
|
||||
</ng-template>
|
||||
</adf-viewer-extension>
|
||||
<adf-viewer-extension [supportedExtensions]="['test']">
|
||||
<ng-template>
|
||||
<h1>Test Viewer</h1>
|
||||
</ng-template>
|
||||
</adf-viewer-extension>
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
class DoubleViewerComponent {
|
||||
@ViewChild('viewer1')
|
||||
viewer1: ViewerRenderComponent;
|
||||
|
||||
@ViewChild('viewer2')
|
||||
viewer2: ViewerRenderComponent;
|
||||
|
||||
@ViewChild('viewerExtension', { static: true })
|
||||
viewerTemplateExtensions: TemplateRef<any>;
|
||||
|
||||
urlFileViewer1: string;
|
||||
urlFileViewer2: string;
|
||||
}
|
||||
|
||||
describe('ViewerComponent', () => {
|
||||
let component: ViewerRenderComponent;
|
||||
let fixture: ComponentFixture<ViewerRenderComponent>;
|
||||
let extensionService: AppExtensionService;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, NoopAnimationsModule, MatDialogModule, ViewerRenderComponent, DoubleViewerComponent],
|
||||
providers: [RenderingQueueServices, { provide: Location, useClass: SpyLocation }, MatDialog],
|
||||
deferBlockBehavior: DeferBlockBehavior.Playthrough
|
||||
});
|
||||
fixture = TestBed.createComponent(ViewerRenderComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
extensionService = TestBed.inject(AppExtensionService);
|
||||
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('Double viewer Test', () => {
|
||||
it('should not reload the content of all the viewer after type change', async () => {
|
||||
const fixtureDouble = TestBed.createComponent(DoubleViewerComponent);
|
||||
|
||||
fixtureDouble.componentInstance.urlFileViewer1 = 'fake-test-file.pdf';
|
||||
fixtureDouble.componentInstance.urlFileViewer2 = 'fake-test-file-two.xls';
|
||||
|
||||
fixtureDouble.detectChanges();
|
||||
await fixtureDouble.whenStable();
|
||||
|
||||
fixtureDouble.componentInstance.viewer1.ngOnChanges();
|
||||
fixtureDouble.componentInstance.viewer2.ngOnChanges();
|
||||
|
||||
fixtureDouble.detectChanges();
|
||||
await fixtureDouble.whenStable();
|
||||
|
||||
expect(fixtureDouble.componentInstance.viewer1.viewerType).toBe('pdf');
|
||||
expect(fixtureDouble.componentInstance.viewer2.viewerType).toBe('unknown');
|
||||
|
||||
fixtureDouble.componentInstance.urlFileViewer1 = 'fake-test-file.pdf';
|
||||
fixtureDouble.componentInstance.urlFileViewer2 = 'fake-test-file-two.png';
|
||||
|
||||
fixtureDouble.detectChanges();
|
||||
await fixtureDouble.whenStable();
|
||||
|
||||
fixtureDouble.componentInstance.viewer1.ngOnChanges();
|
||||
fixtureDouble.componentInstance.viewer2.ngOnChanges();
|
||||
|
||||
expect(fixtureDouble.componentInstance.viewer1.viewerType).toBe('pdf');
|
||||
expect(fixtureDouble.componentInstance.viewer2.viewerType).toBe('image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Type Test', () => {
|
||||
it('should display pdf external viewer via wildcard notation', async () => {
|
||||
const extension: ViewerExtensionRef = {
|
||||
component: 'custom.component',
|
||||
id: 'custom.component.id',
|
||||
fileExtension: '*'
|
||||
};
|
||||
spyOn(extensionService, 'getViewerExtensions').and.returnValue([extension]);
|
||||
|
||||
fixture = TestBed.createComponent(ViewerRenderComponent);
|
||||
testingUtils.setDebugElement(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.externalExtensions.includes('*')).toBe(true);
|
||||
expect(component.externalViewer).toBe(extension);
|
||||
expect(component.viewerType).toBe('external');
|
||||
expect(testingUtils.getByDataAutomationId('custom.component')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display pdf with the first external viewer provided', async () => {
|
||||
const extensions: ViewerExtensionRef[] = [
|
||||
{
|
||||
component: 'custom.component.1',
|
||||
id: 'custom.component.id',
|
||||
fileExtension: '*'
|
||||
},
|
||||
{
|
||||
component: 'custom.component.2',
|
||||
id: 'custom.component.id',
|
||||
fileExtension: '*'
|
||||
}
|
||||
];
|
||||
spyOn(extensionService, 'getViewerExtensions').and.returnValue(extensions);
|
||||
|
||||
fixture = TestBed.createComponent(ViewerRenderComponent);
|
||||
testingUtils.setDebugElement(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('custom.component.1')).not.toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('custom.component.2')).toBeNull();
|
||||
});
|
||||
|
||||
it('should display url with the external viewer provided', async () => {
|
||||
const extension: ViewerExtensionRef = {
|
||||
component: 'custom.component',
|
||||
id: 'custom.component.id',
|
||||
fileExtension: '*'
|
||||
};
|
||||
spyOn(extensionService, 'getViewerExtensions').and.returnValue([extension]);
|
||||
|
||||
fixture = TestBed.createComponent(ViewerRenderComponent);
|
||||
testingUtils.setDebugElement(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.urlFile = 'http://localhost:4200/alfresco';
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.externalExtensions.includes('*')).toBe(true);
|
||||
expect(component.externalViewer).toBe(extension);
|
||||
expect(component.viewerType).toBe('external');
|
||||
expect(testingUtils.getByDataAutomationId('custom.component')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should extension file pdf be loaded', (done) => {
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-pdf-viewer')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should extension file png be loaded', (done) => {
|
||||
component.urlFile = 'fake-url-file.png';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('#viewer-image')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should extension file mp4 be loaded', (done) => {
|
||||
component.urlFile = 'fake-url-file.mp4';
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-media-player')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should extension file txt be loaded', (done) => {
|
||||
component.urlFile = 'fake-test-file.txt';
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-txt-viewer')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display [unknown format] for unsupported extensions', (done) => {
|
||||
component.urlFile = 'fake-url-file.unsupported';
|
||||
component.mimeType = '';
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-viewer-unknown-format')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom viewer extension template', () => {
|
||||
const getCustomViewerContent = (customFixture: ComponentFixture<DoubleViewerComponent>): HTMLHeadingElement => {
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
return testingUtils.getByCSS('.adf-viewer-render-custom-content h1').nativeElement;
|
||||
};
|
||||
|
||||
it('should render provided custom template when file type matches supported extensions', async () => {
|
||||
const fixtureCustom = TestBed.createComponent(DoubleViewerComponent);
|
||||
fixtureCustom.detectChanges();
|
||||
await fixtureCustom.whenStable();
|
||||
|
||||
const customComponent = fixtureCustom.componentInstance.viewer1;
|
||||
fixtureCustom.componentInstance.urlFileViewer1 = 'fake-url-file.json';
|
||||
customComponent.ngOnChanges();
|
||||
|
||||
fixtureCustom.detectChanges();
|
||||
await fixtureCustom.whenStable();
|
||||
|
||||
let customContent = getCustomViewerContent(fixtureCustom);
|
||||
expect(customComponent.extensionsSupportedByTemplates).toEqual(['json', 'test']);
|
||||
expect(customComponent.extensionTemplates.length).toBe(2);
|
||||
expect(customComponent.extensionTemplates[0].isVisible).toBeTrue();
|
||||
expect(customComponent.extensionTemplates[1].isVisible).toBeFalse();
|
||||
expect(customContent.innerText).toBe('JSON Viewer');
|
||||
|
||||
fixtureCustom.componentInstance.urlFileViewer1 = 'fake-url-file.test';
|
||||
customComponent.ngOnChanges();
|
||||
|
||||
fixtureCustom.detectChanges();
|
||||
await fixtureCustom.whenStable();
|
||||
|
||||
customContent = getCustomViewerContent(fixtureCustom);
|
||||
expect(customComponent.extensionTemplates[0].isVisible).toBeFalse();
|
||||
expect(customComponent.extensionTemplates[1].isVisible).toBeTrue();
|
||||
expect(customContent.innerText).toBe('Test Viewer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MimeType handling', () => {
|
||||
it('should display an image file identified by mimetype when the filename has no extension', (done) => {
|
||||
component.urlFile = 'fake-content-img';
|
||||
component.mimeType = 'image/png';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('#viewer-image')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a image file identified by mimetype when the file extension is wrong', (done) => {
|
||||
component.urlFile = 'fake-content-img.bin';
|
||||
component.mimeType = 'image/png';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('#viewer-image')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the txt viewer if the file identified by mimetype is a txt when the filename has wrong extension', (done) => {
|
||||
component.urlFile = 'fake-content-txt.bin';
|
||||
component.mimeType = 'text/plain';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-txt-viewer')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the media player if the file identified by mimetype is a media when the filename has wrong extension', (done) => {
|
||||
component.urlFile = 'fake-content-video.bin';
|
||||
component.mimeType = 'video/mp4';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-media-player')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
}, 25000);
|
||||
|
||||
it('should display the media player if the file identified by mimetype is a media when the filename has no extension', (done) => {
|
||||
component.urlFile = 'fake-content-video';
|
||||
component.mimeType = 'video/mp4';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-media-player')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
}, 25000);
|
||||
|
||||
it('should display a PDF file identified by mimetype when the filename has no extension', (done) => {
|
||||
component.urlFile = 'fake-content-pdf';
|
||||
component.mimeType = 'application/pdf';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
fixture.getDeferBlocks().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-pdf-viewer')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
}, 25000);
|
||||
|
||||
it('should display a PDF file identified by mimetype when the file extension is wrong', (done) => {
|
||||
component.urlFile = 'fake-content-pdf.bin';
|
||||
component.mimeType = 'application/pdf';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-pdf-viewer')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
}, 25000);
|
||||
});
|
||||
|
||||
describe('Base component', () => {
|
||||
beforeEach(() => {
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
component.mimeType = 'application/pdf';
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit new value when isSaving emits new event', () => {
|
||||
spyOn(component.isSaving, 'emit');
|
||||
component.urlFile = 'fake-url-file.png';
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
const imgViewer = testingUtils.getByCSS('adf-img-viewer');
|
||||
imgViewer.triggerEventHandler('isSaving', true);
|
||||
|
||||
expect(component.isSaving.emit).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
describe('Attribute', () => {
|
||||
it('should urlFile present not thrown any error ', () => {
|
||||
expect(() => {
|
||||
component.ngOnChanges();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should switch to the unknown template if the type specific viewers throw an error', (done) => {
|
||||
component.urlFile = 'fake-url-file.icns';
|
||||
component.mimeType = 'image/png';
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
component.onUnsupportedFile();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('adf-viewer-unknown-format')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Events', () => {
|
||||
it('should if the extension change extension Change event be fired ', (done) => {
|
||||
component.extensionChange.subscribe((fileExtension) => {
|
||||
expect(fileExtension).toEqual('png');
|
||||
done();
|
||||
});
|
||||
|
||||
component.urlFile = 'fake-url-file.png';
|
||||
|
||||
component.ngOnChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe('display name property override by urlFile', () => {
|
||||
it('should fileName override the default name if is present and urlFile is set', () => {
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
component.fileName = 'test name';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
expect(component.internalFileName).toEqual('test name');
|
||||
});
|
||||
|
||||
it('should use the urlFile name if fileName is NOT set and urlFile is set', () => {
|
||||
component.urlFile = 'fake-test-file.pdf';
|
||||
component.fileName = '';
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
expect(component.internalFileName).toEqual('fake-test-file.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('display name property override by blobFile', () => {
|
||||
it('should fileName override the name if is present and blobFile is set', () => {
|
||||
component.fileName = 'blob file display name';
|
||||
component.blobFile = new Blob(['This is my blob content'], { type: 'text/plain' });
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
|
||||
expect(component.internalFileName).toEqual('blob file display name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Spinner', () => {
|
||||
const getMainLoader = (): DebugElement => testingUtils.getByCSS('.adf-viewer-render-main-loader');
|
||||
|
||||
it('should not show spinner by default', (done) => {
|
||||
component.isLoading$.subscribe((isLoading) => {
|
||||
fixture.detectChanges();
|
||||
expect(isLoading).toBeFalse();
|
||||
expect(getMainLoader()).toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display spinner when viewerType is media', () => {
|
||||
component.urlFile = 'some-file.mp4';
|
||||
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getMainLoader()).not.toBeNull();
|
||||
|
||||
const mediaViewer = testingUtils.getByDirective(MediaPlayerComponent);
|
||||
mediaViewer.triggerEventHandler('canPlay', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getMainLoader()).toBeNull();
|
||||
expect(component.viewerType).toBe('media');
|
||||
});
|
||||
|
||||
it('should display spinner when viewerType is pdf', () => {
|
||||
component.urlFile = 'some-url.pdf';
|
||||
expect(getMainLoader()).toBeNull();
|
||||
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
const imgViewer = testingUtils.getByDirective(PdfViewerComponent);
|
||||
imgViewer.triggerEventHandler('pagesLoaded', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.viewerType).toBe('pdf');
|
||||
});
|
||||
|
||||
it('should display spinner when viewerType is image', () => {
|
||||
component.urlFile = 'some-url.png';
|
||||
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
expect(getMainLoader()).not.toBeNull();
|
||||
|
||||
const imgViewer = testingUtils.getByDirective(ImgViewerComponent);
|
||||
imgViewer.triggerEventHandler('imageLoaded', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getMainLoader()).toBeNull();
|
||||
expect(component.viewerType).toBe('image');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,250 @@
|
||||
/*!
|
||||
* @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 { AppExtensionService, ExtensionsModule, ViewerExtensionRef } from '@alfresco/adf-extensions';
|
||||
import { AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, EventEmitter, Injector, Input, OnChanges, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Track } from '../../models/viewer.model';
|
||||
import { ViewUtilService } from '../../services/view-util.service';
|
||||
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';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
type ViewerType = 'media' | 'image' | 'pdf' | 'external' | 'text' | 'custom' | 'unknown';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-render',
|
||||
standalone: true,
|
||||
templateUrl: './viewer-render.component.html',
|
||||
styleUrls: ['./viewer-render.component.scss'],
|
||||
host: { class: 'adf-viewer-render' },
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [
|
||||
TranslateModule,
|
||||
MatProgressSpinnerModule,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgIf,
|
||||
PdfViewerComponent,
|
||||
ImgViewerComponent,
|
||||
MediaPlayerComponent,
|
||||
TxtViewerComponent,
|
||||
NgTemplateOutlet,
|
||||
UnknownFormatComponent,
|
||||
ExtensionsModule,
|
||||
NgForOf,
|
||||
NgSwitchDefault,
|
||||
AsyncPipe
|
||||
],
|
||||
providers: [ViewUtilService]
|
||||
})
|
||||
export class ViewerRenderComponent implements OnChanges, OnInit {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Input()
|
||||
urlFile = '';
|
||||
|
||||
/** Loads a Blob File */
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
/** Toggles the 'Full Screen' feature. */
|
||||
@Input()
|
||||
allowFullScreen = true;
|
||||
|
||||
/** Toggles PDF thumbnails. */
|
||||
@Input()
|
||||
allowThumbnails = true;
|
||||
|
||||
/** The template for the pdf thumbnails. */
|
||||
@Input()
|
||||
thumbnailsTemplate: TemplateRef<any> = null;
|
||||
|
||||
/** MIME type of the file content (when not determined by the filename extension). */
|
||||
@Input()
|
||||
mimeType: string;
|
||||
|
||||
/** Override Content filename. */
|
||||
@Input()
|
||||
fileName: string;
|
||||
|
||||
/** Enable when where is possible the editing functionalities */
|
||||
@Input()
|
||||
readOnly = true;
|
||||
|
||||
/**
|
||||
* Controls which actions are enabled in the viewer.
|
||||
* Example:
|
||||
* { rotate: true, crop: false } will enable rotation but disable cropping.
|
||||
*/
|
||||
@Input()
|
||||
allowedEditActions: { [key: string]: boolean } = {
|
||||
rotate: true,
|
||||
crop: true
|
||||
};
|
||||
|
||||
/** media subtitles for the media player*/
|
||||
@Input()
|
||||
tracks: Track[] = [];
|
||||
|
||||
/** Identifier of a node that is opened by the viewer. */
|
||||
@Input()
|
||||
nodeId: string = null;
|
||||
|
||||
/** Template containing ViewerExtensionDirective instances providing different viewer extensions based on supported file extension. */
|
||||
@Input()
|
||||
viewerTemplateExtensions: TemplateRef<any>;
|
||||
|
||||
/** Custom error message to be displayed in the viewer. */
|
||||
@Input()
|
||||
customError: string = undefined;
|
||||
|
||||
/** Emitted when the filename extension changes. */
|
||||
@Output()
|
||||
extensionChange = new EventEmitter<string>();
|
||||
|
||||
/** Emitted when the img is submitted in the img viewer. */
|
||||
@Output()
|
||||
submitFile = new EventEmitter<Blob>();
|
||||
|
||||
/** Emitted when the img is submitted in the img viewer. */
|
||||
@Output()
|
||||
close = new EventEmitter<boolean>();
|
||||
|
||||
/** Emitted when the img is saving. */
|
||||
@Output()
|
||||
isSaving = new EventEmitter<boolean>();
|
||||
|
||||
extensionTemplates: { template: TemplateRef<any>; isVisible: boolean }[] = [];
|
||||
extensionsSupportedByTemplates: string[] = [];
|
||||
extension: string;
|
||||
internalFileName: string;
|
||||
viewerType: ViewerType = 'unknown';
|
||||
readonly isLoading$ = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* Returns a list of the active Viewer content extensions.
|
||||
*
|
||||
* @returns list of extension references
|
||||
*/
|
||||
get viewerExtensions(): ViewerExtensionRef[] {
|
||||
return this.extensionService.getViewerExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of file extensions supported by external plugins.
|
||||
*
|
||||
* @returns list of extensions
|
||||
*/
|
||||
get externalExtensions(): string[] {
|
||||
return this.viewerExtensions.map((ext) => ext.fileExtension);
|
||||
}
|
||||
|
||||
private _externalViewer: ViewerExtensionRef;
|
||||
get externalViewer(): ViewerExtensionRef {
|
||||
if (!this._externalViewer) {
|
||||
this._externalViewer = this.viewerExtensions.find((ext) => ext.fileExtension === '*');
|
||||
}
|
||||
|
||||
return this._externalViewer;
|
||||
}
|
||||
|
||||
cacheTypeForContent = 'no-cache';
|
||||
|
||||
constructor(
|
||||
private viewUtilService: ViewUtilService,
|
||||
private extensionService: AppExtensionService,
|
||||
public dialog: MatDialog,
|
||||
public readonly injector: Injector
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.cacheTypeForContent = 'no-cache';
|
||||
this.setDefaultLoadingState();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.blobFile) {
|
||||
this.setUpBlobData();
|
||||
} else if (this.urlFile) {
|
||||
this.setUpUrlFile();
|
||||
}
|
||||
}
|
||||
|
||||
markAsLoaded() {
|
||||
this.isLoading$.next(false);
|
||||
}
|
||||
|
||||
private setUpBlobData() {
|
||||
this.internalFileName = this.fileName;
|
||||
this.viewerType = this.viewUtilService.getViewerTypeByMimeType(this.blobFile.type) as ViewerType;
|
||||
|
||||
this.extensionChange.emit(this.blobFile.type);
|
||||
this.scrollTop();
|
||||
}
|
||||
|
||||
private setUpUrlFile() {
|
||||
this.internalFileName = this.fileName ? this.fileName : this.viewUtilService.getFilenameFromUrl(this.urlFile);
|
||||
this.extension = this.viewUtilService.getFileExtension(this.internalFileName);
|
||||
this.viewerType = this.viewUtilService.getViewerType(this.extension, this.mimeType, this.extensionsSupportedByTemplates) as ViewerType;
|
||||
|
||||
this.extensionChange.emit(this.extension);
|
||||
this.scrollTop();
|
||||
}
|
||||
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 1);
|
||||
}
|
||||
|
||||
checkExtensions(extensionAllowed) {
|
||||
if (typeof extensionAllowed === 'string') {
|
||||
return this.extension.toLowerCase() === extensionAllowed.toLowerCase();
|
||||
} else if (extensionAllowed.length > 0) {
|
||||
return extensionAllowed.find((currentExtension) => this.extension.toLowerCase() === currentExtension.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitFile(newImageBlob: Blob) {
|
||||
this.submitFile.next(newImageBlob);
|
||||
}
|
||||
|
||||
onUnsupportedFile() {
|
||||
this.viewerType = 'unknown';
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.close.next(true);
|
||||
}
|
||||
|
||||
private canBePreviewed(): boolean {
|
||||
return this.viewerType === 'media' || this.viewerType === 'pdf' || this.viewerType === 'image';
|
||||
}
|
||||
|
||||
private setDefaultLoadingState() {
|
||||
if (this.canBePreviewed()) {
|
||||
this.isLoading$.next(true);
|
||||
}
|
||||
}
|
||||
}
|
38
lib/core/viewer/src/components/viewer-sidebar.component.ts
Normal file
38
lib/core/viewer/src/components/viewer-sidebar.component.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, HostListener, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-sidebar',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-sidebar' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerSidebarComponent {
|
||||
@HostListener('keydown', ['$event'])
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('keyup', ['$event'])
|
||||
onKeyUp(event: KeyboardEvent): void {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-toolbar-actions',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-toolbar-actions' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerToolbarActionsComponent {}
|
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-toolbar-custom-actions',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-toolbar-custom-actions' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerToolbarCustomActionsComponent {}
|
28
lib/core/viewer/src/components/viewer-toolbar.component.ts
Normal file
28
lib/core/viewer/src/components/viewer-toolbar.component.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* @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 { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer-toolbar',
|
||||
standalone: true,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'adf-viewer-toolbar' },
|
||||
template: `<ng-content />`
|
||||
})
|
||||
export class ViewerToolbarComponent {}
|
182
lib/core/viewer/src/components/viewer.component.html
Normal file
182
lib/core/viewer/src/components/viewer.component.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<div *ngIf="showViewer"
|
||||
class="adf-viewer-container"
|
||||
[class.adf-viewer-overlay-container]="overlayMode"
|
||||
[class.adf-viewer-inline-container]="!overlayMode">
|
||||
|
||||
<div class="adf-viewer-content"
|
||||
[cdkTrapFocus]="overlayMode"
|
||||
cdkTrapFocusAutoCapture>
|
||||
<ng-content select="adf-viewer-toolbar" />
|
||||
<ng-container *ngIf="showToolbar && !toolbar">
|
||||
<adf-toolbar id="adf-viewer-toolbar" class="adf-viewer-toolbar">
|
||||
<adf-toolbar-title>
|
||||
<ng-container *ngIf="allowLeftSidebar">
|
||||
<button mat-icon-button
|
||||
[attr.aria-expanded]="showLeftSidebar"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.INFO' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.INFO' | translate }}"
|
||||
data-automation-id="adf-toolbar-left-sidebar"
|
||||
[color]="showLeftSidebar ? 'accent' : null"
|
||||
(click)="toggleLeftSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button *ngIf="allowGoBack && closeButtonPosition === CloseButtonPosition.Left"
|
||||
class="adf-viewer-close-button"
|
||||
data-automation-id="adf-toolbar-left-back"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
|
||||
mat-icon-button
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}"
|
||||
(click)="onClose()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</adf-toolbar-title>
|
||||
|
||||
<div class="adf-viewer__file-title">
|
||||
<button *ngIf="allowNavigate && canNavigateBefore"
|
||||
data-automation-id="adf-toolbar-pref-file"
|
||||
mat-icon-button
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.PREV_FILE' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.PREV_FILE' | translate }}"
|
||||
(click)="onNavigateBeforeClick($event)">
|
||||
<mat-icon>navigate_before</mat-icon>
|
||||
</button>
|
||||
<img class="adf-viewer__mimeicon"
|
||||
[alt]="'ADF_VIEWER.ARIA.MIME_TYPE_ICON' | translate"
|
||||
[src]="mimeTypeIconUrl"
|
||||
data-automation-id="adf-file-thumbnail">
|
||||
<div class="adf-viewer__display-name"
|
||||
id="adf-viewer-display-name"
|
||||
[title]="fileName">
|
||||
<span class="adf-viewer__display-name-without-extension">{{ fileNameWithoutExtension }}</span>
|
||||
<span class="adf-viewer__display-name-extension">{{ fileExtension }}</span>
|
||||
</div>
|
||||
<button *ngIf="allowNavigate && canNavigateNext"
|
||||
data-automation-id="adf-toolbar-next-file"
|
||||
mat-icon-button
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.NEXT_FILE' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.NEXT_FILE' | translate }}"
|
||||
(click)="onNavigateNextClick($event)">
|
||||
<mat-icon>navigate_next</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-content select="adf-viewer-toolbar-actions" />
|
||||
|
||||
<ng-container *ngIf="mnuOpenWith"
|
||||
data-automation-id='adf-toolbar-custom-btn'>
|
||||
<button id="adf-viewer-openwith"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="mnuOpenWith"
|
||||
data-automation-id="adf-toolbar-open-with">
|
||||
<span>{{ 'ADF_VIEWER.ACTIONS.OPEN_WITH' | translate }}</span>
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<mat-menu #mnuOpenWith="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<ng-content select="adf-viewer-open-with" />
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<adf-toolbar-divider />
|
||||
|
||||
<ng-content select="adf-viewer-toolbar-custom-actions" />
|
||||
|
||||
<button id="adf-viewer-fullscreen"
|
||||
*ngIf="allowFullScreen"
|
||||
mat-icon-button
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate }}"
|
||||
data-automation-id="adf-toolbar-fullscreen"
|
||||
(click)="enterFullScreen()">
|
||||
<mat-icon>fullscreen</mat-icon>
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="allowRightSidebar && !hideInfoButton">
|
||||
<adf-toolbar-divider />
|
||||
|
||||
<button mat-icon-button
|
||||
[attr.aria-expanded]="showRightSidebar"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.INFO' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.INFO' | translate }}"
|
||||
data-automation-id="adf-toolbar-sidebar"
|
||||
[color]="showRightSidebar ? 'accent' : null"
|
||||
(click)="toggleRightSidebar()">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
</button>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="mnuMoreActions">
|
||||
<button id="adf-viewer-moreactions"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="mnuMoreActions"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.MORE_ACTIONS' | translate"
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.MORE_ACTIONS' | translate }}"
|
||||
data-automation-id="adf-toolbar-more-actions">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #mnuMoreActions="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<ng-content select="adf-viewer-more-actions" />
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="allowGoBack && closeButtonPosition === CloseButtonPosition.Right">
|
||||
<adf-toolbar-divider />
|
||||
|
||||
<button class="adf-viewer-close-button"
|
||||
data-automation-id="adf-toolbar-right-back"
|
||||
[attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
|
||||
mat-icon-button
|
||||
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}"
|
||||
(click)="onClose()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
</adf-toolbar>
|
||||
</ng-container>
|
||||
|
||||
<div class="adf-viewer-sidebars">
|
||||
|
||||
<ng-container *ngIf="allowRightSidebar && showRightSidebar">
|
||||
<div class="adf-viewer__sidebar adf-viewer__sidebar__right"
|
||||
id="adf-right-sidebar">
|
||||
<ng-container *ngIf="sidebarRightTemplate">
|
||||
<ng-container *ngTemplateOutlet="sidebarRightTemplate;context:sidebarRightTemplateContext" />
|
||||
</ng-container>
|
||||
<ng-content *ngIf="!sidebarRightTemplate"
|
||||
select="adf-viewer-sidebar" />
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="allowLeftSidebar && showLeftSidebar">
|
||||
<div class="adf-viewer__sidebar adf-viewer__sidebar__left"
|
||||
id="adf-left-sidebar">
|
||||
<ng-container *ngIf="sidebarLeftTemplate">
|
||||
<ng-container *ngTemplateOutlet="sidebarLeftTemplate;context:sidebarLeftTemplateContext" />
|
||||
</ng-container>
|
||||
<ng-content *ngIf="!sidebarLeftTemplate"
|
||||
select="adf-viewer-sidebar" />
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<adf-viewer-render (close)="onClose()"
|
||||
[mimeType]="mimeType"
|
||||
[fileName]="fileName"
|
||||
[blobFile]="blobFile"
|
||||
[readOnly]="readOnly"
|
||||
(submitFile)="onSubmitFile($event)"
|
||||
[allowedEditActions]="allowedEditActions"
|
||||
[urlFile]="urlFile"
|
||||
(isSaving)="allowNavigate = !$event"
|
||||
[tracks]="tracks"
|
||||
[viewerTemplateExtensions]="viewerExtensions ?? viewerTemplateExtensions"
|
||||
[nodeId]="nodeId"
|
||||
[customError]="customError" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
164
lib/core/viewer/src/components/viewer.component.scss
Normal file
164
lib/core/viewer/src/components/viewer.component.scss
Normal file
@@ -0,0 +1,164 @@
|
||||
/* stylelint-disable scss/at-extend-no-missing-placeholder */
|
||||
@use 'mat-selectors' as ms;
|
||||
|
||||
.adf-full-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--adf-theme-background-card-color);
|
||||
}
|
||||
|
||||
.adf-viewer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
#{ms.$mat-toolbar} {
|
||||
color: var(--adf-theme-foreground-text-color-054);
|
||||
|
||||
.adf-toolbar-title {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&__mimeicon {
|
||||
vertical-align: middle;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
#{ms.$mat-toolbar} {
|
||||
background-color: var(--adf-theme-background-card-color-087);
|
||||
}
|
||||
}
|
||||
|
||||
&__file-title {
|
||||
text-align: center;
|
||||
flex: 1 1 auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (width <= 1450px) {
|
||||
left: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
&__display-name {
|
||||
font-size: var(--theme-subheading-2-font-size);
|
||||
opacity: 0.87;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.4px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
vertical-align: middle;
|
||||
color: var(--adf-theme-foreground-text-color);
|
||||
|
||||
&-without-extension {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
.adf-viewer-layout-content {
|
||||
@extend .adf-full-screen;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
background-color: var(--theme-background-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-viewer-layout {
|
||||
@extend .adf-full-screen;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.adf-viewer-content {
|
||||
@extend .adf-full-screen;
|
||||
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
|
||||
& > div {
|
||||
height: 0; // Firefox
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-overlay-container {
|
||||
.adf-viewer-content {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
&-inline-container {
|
||||
@extend .adf-full-screen;
|
||||
}
|
||||
|
||||
&-sidebars {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.adf-viewer__sidebar {
|
||||
width: 350px;
|
||||
display: block;
|
||||
padding: 0;
|
||||
background-color: var(--theme-background-color);
|
||||
box-shadow: 0 2px 4px 0 var(--adf-theme-foreground-text-color-027);
|
||||
overflow: auto;
|
||||
|
||||
&__right {
|
||||
border-left: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||
order: 4;
|
||||
}
|
||||
|
||||
&__left {
|
||||
border-right: 1px solid var(--adf-theme-foreground-text-color-007);
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
adf-viewer-render {
|
||||
order: 1;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
673
lib/core/viewer/src/components/viewer.component.spec.ts
Normal file
673
lib/core/viewer/src/components/viewer.component.spec.ts
Normal file
@@ -0,0 +1,673 @@
|
||||
/*!
|
||||
* @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 { Component } from '@angular/core';
|
||||
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { of } from 'rxjs';
|
||||
import { AppConfigService } from '../../app-config';
|
||||
import { EventMock } from '../../mock';
|
||||
import { CoreTestingModule, UnitTestingUtils } from '../../testing';
|
||||
import { DownloadPromptActions } from '../models/download-prompt.actions';
|
||||
import { CloseButtonPosition } from '../models/viewer.model';
|
||||
import { ViewUtilService } from '../services/view-util.service';
|
||||
import { DownloadPromptDialogComponent } from './download-prompt-dialog/download-prompt-dialog.component';
|
||||
import { ViewerWithCustomMoreActionsComponent } from './mock/adf-viewer-container-more-actions.component.mock';
|
||||
import { ViewerWithCustomOpenWithComponent } from './mock/adf-viewer-container-open-with.component.mock';
|
||||
import { ViewerWithCustomSidebarComponent } from './mock/adf-viewer-container-sidebar.component.mock';
|
||||
import { ViewerWithCustomToolbarActionsComponent } from './mock/adf-viewer-container-toolbar-actions.component.mock';
|
||||
import { ViewerWithCustomToolbarComponent } from './mock/adf-viewer-container-toolbar.component.mock';
|
||||
import { ViewerComponent } from './viewer.component';
|
||||
import { ThumbnailService } from '../../common/services/thumbnail.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-dialog-dummy',
|
||||
template: ``
|
||||
})
|
||||
class DummyDialogComponent {}
|
||||
|
||||
describe('ViewerComponent', () => {
|
||||
let component: ViewerComponent<any>;
|
||||
let fixture: ComponentFixture<ViewerComponent<any>>;
|
||||
let dialog: MatDialog;
|
||||
let viewUtilService: ViewUtilService;
|
||||
let appConfigService: AppConfigService;
|
||||
let thumbnailService: ThumbnailService;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
const getFileName = (): string => testingUtils.getByCSS('#adf-viewer-display-name').nativeElement.textContent;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CoreTestingModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
ViewerWithCustomToolbarComponent,
|
||||
ViewerWithCustomSidebarComponent,
|
||||
ViewerWithCustomOpenWithComponent,
|
||||
ViewerWithCustomMoreActionsComponent,
|
||||
ViewerWithCustomToolbarActionsComponent
|
||||
],
|
||||
providers: [MatDialog, { provide: DownloadPromptDialogComponent, useClass: DummyDialogComponent }]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(ViewerComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
viewUtilService = TestBed.inject(ViewUtilService);
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
thumbnailService = TestBed.inject(ThumbnailService);
|
||||
component.fileName = 'test-file.pdf';
|
||||
|
||||
appConfigService.config = {
|
||||
...appConfigService.config,
|
||||
viewer: {
|
||||
enableDownloadPrompt: false,
|
||||
enableDownloadPromptReminder: false,
|
||||
downloadPromptDelay: 3,
|
||||
downloadPromptReminderDelay: 2
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('Mime Type Test', () => {
|
||||
it('should mimeType change when blobFile changes', () => {
|
||||
const mockSimpleChanges: any = { blobFile: { currentValue: { type: 'image/png' } } };
|
||||
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
|
||||
expect(component.mimeType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should set mimeTypeIconUrl when mimeType changes and no nodeMimeType is provided', () => {
|
||||
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('image/png');
|
||||
const mockSimpleChanges: any = { mimeType: { currentValue: 'image/png' }, nodeMimeType: undefined };
|
||||
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
|
||||
expect(thumbnailService.getMimeTypeIcon).toHaveBeenCalledWith('image/png');
|
||||
expect(component.mimeTypeIconUrl).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should set mimeTypeIconUrl when nodeMimeType changes', () => {
|
||||
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('application/pdf');
|
||||
const mockSimpleChanges: any = { mimeType: { currentValue: 'image/png' }, nodeMimeType: { currentValue: 'application/pdf' } };
|
||||
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(thumbnailService.getMimeTypeIcon).toHaveBeenCalledWith('application/pdf');
|
||||
expect(component.mimeTypeIconUrl).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should reset urlFile and blobFile on onNavigateBeforeClick', () => {
|
||||
component.urlFile = 'some-url';
|
||||
component.blobFile = new Blob(['content'], { type: 'text/plain' });
|
||||
|
||||
component.onNavigateBeforeClick(new MouseEvent('click'));
|
||||
|
||||
expect(component.urlFile).toBe('');
|
||||
expect(component.blobFile).toBeNull();
|
||||
});
|
||||
|
||||
it('should reset urlFile and blobFile on onNavigateNextClick', () => {
|
||||
component.urlFile = 'some-url';
|
||||
component.blobFile = new Blob(['content'], { type: 'text/plain' });
|
||||
|
||||
component.onNavigateNextClick(new MouseEvent('click'));
|
||||
|
||||
expect(component.urlFile).toBe('');
|
||||
expect(component.blobFile).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Name Test', () => {
|
||||
const getFileNameWithoutExtension = (): string =>
|
||||
testingUtils.getByCSS('.adf-viewer__display-name-without-extension').nativeElement.textContent;
|
||||
const getExtension = (): string => testingUtils.getByCSS('.adf-viewer__display-name-extension').nativeElement.textContent;
|
||||
|
||||
it('should fileName be set by urlFile input if the fileName is not provided as Input', () => {
|
||||
component.fileName = '';
|
||||
spyOn(viewUtilService, 'getFilenameFromUrl').and.returnValue('fakeFileName.jpeg');
|
||||
const mockSimpleChanges: any = { urlFile: { currentValue: 'https://fakefile.url/fakeFileName.jpeg' } };
|
||||
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFileName()).toEqual('fakeFileName.jpeg');
|
||||
expect(getFileNameWithoutExtension()).toBe('fakeFileName.');
|
||||
expect(getExtension()).toBe('jpeg');
|
||||
});
|
||||
|
||||
it('should set fileName providing fileName input', () => {
|
||||
component.fileName = 'testFileName.jpg';
|
||||
spyOn(viewUtilService, 'getFilenameFromUrl').and.returnValue('fakeFileName.jpeg');
|
||||
const mockSimpleChanges: any = { urlFile: { currentValue: 'https://fakefile.url/fakeFileName.jpeg' } };
|
||||
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFileName()).toEqual('testFileName.jpg');
|
||||
expect(getFileNameWithoutExtension()).toBe('testFileName.');
|
||||
expect(getExtension()).toBe('jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Viewer Example Component Rendering', () => {
|
||||
it('should use custom toolbar', (done) => {
|
||||
const customFixture = TestBed.createComponent(ViewerWithCustomToolbarComponent);
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
customFixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('.custom-toolbar-element')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom toolbar actions', (done) => {
|
||||
const customFixture = TestBed.createComponent(ViewerWithCustomToolbarActionsComponent);
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
customFixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('#custom-button')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom info drawer', (done) => {
|
||||
const customFixture = TestBed.createComponent(ViewerWithCustomSidebarComponent);
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
customFixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('.custom-info-drawer-element')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom open with menu', (done) => {
|
||||
const customFixture = TestBed.createComponent(ViewerWithCustomOpenWithComponent);
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
customFixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('.adf-viewer-container-open-with')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom more actions menu', (done) => {
|
||||
const customFixture = TestBed.createComponent(ViewerWithCustomMoreActionsComponent);
|
||||
testingUtils.setDebugElement(customFixture.debugElement);
|
||||
customFixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('.adf-viewer-container-more-actions')).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toolbar', () => {
|
||||
it('should show only next file button', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = false;
|
||||
component.canNavigateNext = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file')).not.toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file')).toBeNull();
|
||||
});
|
||||
|
||||
it('should provide tooltip for next file button', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = false;
|
||||
component.canNavigateNext = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file').nativeElement.title).toBe('ADF_VIEWER.ACTIONS.NEXT_FILE');
|
||||
});
|
||||
|
||||
it('should show only previous file button', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = true;
|
||||
component.canNavigateNext = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file')).toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should provide tooltip for the previous file button', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = true;
|
||||
component.canNavigateNext = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file').nativeElement.title).toBe('ADF_VIEWER.ACTIONS.PREV_FILE');
|
||||
});
|
||||
|
||||
it('should show both file navigation buttons', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = true;
|
||||
component.canNavigateNext = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file')).not.toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not show navigation buttons', async () => {
|
||||
component.allowNavigate = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file')).toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not show navigation buttons if file is saving', async () => {
|
||||
component.allowNavigate = true;
|
||||
fixture.detectChanges();
|
||||
const viewerRender = testingUtils.getByCSS('adf-viewer-render');
|
||||
|
||||
viewerRender.triggerEventHandler('isSaving', true);
|
||||
expect(component.allowNavigate).toBeFalsy();
|
||||
|
||||
viewerRender.triggerEventHandler('isSaving', false);
|
||||
expect(component.allowNavigate).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should now show navigation buttons even if navigation enabled', async () => {
|
||||
component.allowNavigate = true;
|
||||
component.canNavigateBefore = false;
|
||||
component.canNavigateNext = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-next-file')).toBeNull();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-pref-file')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render fullscreen button', () => {
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-fullscreen')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render close viewer button if it is not a shared link', (done) => {
|
||||
component.closeButtonPosition = CloseButtonPosition.Left;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByDataAutomationId('adf-toolbar-left-back')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base component', () => {
|
||||
beforeEach(() => {
|
||||
component.mimeType = 'application/pdf';
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('SideBar Test', () => {
|
||||
it('should NOT display sidebar if is not allowed', (done) => {
|
||||
component.showRightSidebar = true;
|
||||
component.allowRightSidebar = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('#adf-right-sidebar')).toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display sidebar on the right side', (done) => {
|
||||
component.allowRightSidebar = true;
|
||||
component.showRightSidebar = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
const sidebar = testingUtils.getByCSS('#adf-right-sidebar').nativeElement;
|
||||
expect(getComputedStyle(sidebar).order).toEqual('4');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT display left sidebar if is not allowed', (done) => {
|
||||
component.showLeftSidebar = true;
|
||||
component.allowLeftSidebar = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('#adf-left-sidebar')).toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display sidebar on the left side', (done) => {
|
||||
component.allowLeftSidebar = true;
|
||||
component.showLeftSidebar = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
const sidebar = testingUtils.getByCSS('#adf-left-sidebar').nativeElement;
|
||||
expect(getComputedStyle(sidebar).order).toEqual('1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Info Button', () => {
|
||||
const infoButton = () => testingUtils.getByDataAutomationId('adf-toolbar-sidebar');
|
||||
|
||||
it('should NOT display info button on the right side', () => {
|
||||
component.allowRightSidebar = true;
|
||||
component.hideInfoButton = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(infoButton()).toBeNull();
|
||||
});
|
||||
|
||||
it('should display info button on the right side', () => {
|
||||
component.allowRightSidebar = true;
|
||||
component.hideInfoButton = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(infoButton()).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
describe('Overlay mode true', () => {
|
||||
beforeEach(() => {
|
||||
component.overlayMode = true;
|
||||
component.fileName = 'fake-test-file.pdf';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should header be present if is overlay mode', () => {
|
||||
expect(testingUtils.getByCSS('.adf-viewer-toolbar')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should file name be present if is overlay mode ', async () => {
|
||||
const mockSimpleChanges: any = { blobFile: { currentValue: { type: 'image/png' } } };
|
||||
component.ngOnChanges(mockSimpleChanges);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(getFileName()).toEqual('fake-test-file.pdf');
|
||||
});
|
||||
|
||||
it('should Close button be present if overlay mode', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-close-button')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should Click on close button hide the viewer', async () => {
|
||||
testingUtils.clickByCSS('.adf-viewer-close-button');
|
||||
fixture.detectChanges();
|
||||
|
||||
await fixture.whenStable();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).toBeNull();
|
||||
});
|
||||
|
||||
it('should Esc button hide the viewer', async () => {
|
||||
EventMock.keyDown(27);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
await fixture.whenStable();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not close the viewer on Escape event if dialog was opened', (done) => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
keyCode: 27
|
||||
} as KeyboardEventInit);
|
||||
|
||||
const dialogRef = dialog.open(DummyDialogComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
EventMock.keyDown(27);
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
document.body.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Overlay mode false', () => {
|
||||
beforeEach(() => {
|
||||
component.overlayMode = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should Esc button not hide the viewer if is not overlay mode', (done) => {
|
||||
EventMock.keyDown(27);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Attribute', () => {
|
||||
it('should showViewer default value be true', () => {
|
||||
expect(component.showViewer).toBe(true);
|
||||
});
|
||||
|
||||
it('should viewer be hide if showViewer value is false', () => {
|
||||
component.showViewer = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(testingUtils.getByCSS('.adf-viewer-content')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close Button', () => {
|
||||
const getRightCloseButton = () => testingUtils.getByDataAutomationId('adf-toolbar-right-back');
|
||||
const getLeftCloseButton = () => testingUtils.getByDataAutomationId('adf-toolbar-left-back');
|
||||
|
||||
it('should show close button on left side when closeButtonPosition is left and allowGoBack is true', () => {
|
||||
component.allowGoBack = true;
|
||||
component.closeButtonPosition = CloseButtonPosition.Left;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getLeftCloseButton()).not.toBeNull();
|
||||
expect(getRightCloseButton()).toBeNull();
|
||||
});
|
||||
|
||||
it('should show close button on right side when closeButtonPosition is right and allowGoBack is true', () => {
|
||||
component.allowGoBack = true;
|
||||
component.closeButtonPosition = CloseButtonPosition.Right;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getRightCloseButton()).not.toBeNull();
|
||||
expect(getLeftCloseButton()).toBeNull();
|
||||
});
|
||||
|
||||
it('should hide close button allowGoBack is false', () => {
|
||||
component.allowGoBack = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getRightCloseButton()).toBeNull();
|
||||
expect(getLeftCloseButton()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Viewer component - Full Screen Mode - Mocking fixture element', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ViewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.showToolbar = true;
|
||||
component.mimeType = 'application/pdf';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should request only if enabled', () => {
|
||||
const domElement = jasmine.createSpyObj('el', ['requestFullscreen']);
|
||||
spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement);
|
||||
|
||||
component.allowFullScreen = false;
|
||||
component.enterFullScreen();
|
||||
|
||||
expect(domElement.requestFullscreen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use standard mode', () => {
|
||||
const domElement = jasmine.createSpyObj('el', ['requestFullscreen']);
|
||||
spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement);
|
||||
|
||||
component.enterFullScreen();
|
||||
expect(domElement.requestFullscreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use webkit prefix', () => {
|
||||
const domElement = jasmine.createSpyObj('el', ['webkitRequestFullscreen']);
|
||||
spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement);
|
||||
|
||||
component.enterFullScreen();
|
||||
expect(domElement.webkitRequestFullscreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use moz prefix', () => {
|
||||
const domElement = jasmine.createSpyObj('el', ['mozRequestFullScreen']);
|
||||
spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement);
|
||||
|
||||
component.enterFullScreen();
|
||||
expect(domElement.mozRequestFullScreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use ms prefix', () => {
|
||||
const domElement = jasmine.createSpyObj('el', ['msRequestFullscreen']);
|
||||
spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement);
|
||||
|
||||
component.enterFullScreen();
|
||||
expect(domElement.msRequestFullscreen).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Download Prompt Dialog', () => {
|
||||
let dialogOpenSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
appConfigService.config = {
|
||||
...appConfigService.config,
|
||||
viewer: {
|
||||
enableDownloadPrompt: true,
|
||||
enableDownloadPromptReminder: true,
|
||||
downloadPromptDelay: 3,
|
||||
downloadPromptReminderDelay: 2
|
||||
}
|
||||
};
|
||||
dialogOpenSpy = spyOn(dialog, 'open').and.returnValue({ afterClosed: () => of(null) } as any);
|
||||
component.urlFile = undefined;
|
||||
component.clearDownloadPromptTimeouts();
|
||||
});
|
||||
|
||||
it('should configure initial timeout to display non responsive dialog when initialising component', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.downloadPromptTimer).toBeDefined();
|
||||
});
|
||||
|
||||
it('should configure reminder timeout to display non responsive dialog after initial dialog', fakeAsync(() => {
|
||||
dialogOpenSpy.and.returnValue({ afterClosed: () => of(DownloadPromptActions.WAIT) } as any);
|
||||
fixture.detectChanges();
|
||||
tick(3000);
|
||||
expect(component.downloadPromptReminderTimer).toBeDefined();
|
||||
dialogOpenSpy.and.returnValue({ afterClosed: () => of(null) } as any);
|
||||
flush();
|
||||
discardPeriodicTasks();
|
||||
}));
|
||||
|
||||
it('should show initial non responsive dialog after initial timeout', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
tick(3000);
|
||||
fixture.detectChanges();
|
||||
expect(dialogOpenSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not show non responsive dialog if blobFile was provided', fakeAsync(() => {
|
||||
component.blobFile = new Blob(['mock content'], { type: 'text/plain' });
|
||||
fixture.detectChanges();
|
||||
tick(3000);
|
||||
fixture.detectChanges();
|
||||
expect(dialogOpenSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should show reminder non responsive dialog after initial dialog', fakeAsync(() => {
|
||||
dialogOpenSpy.and.returnValue({ afterClosed: () => of(DownloadPromptActions.WAIT) } as any);
|
||||
fixture.detectChanges();
|
||||
tick(3000);
|
||||
expect(dialogOpenSpy).toHaveBeenCalled();
|
||||
|
||||
dialogOpenSpy.and.returnValue({ afterClosed: () => of(null) } as any);
|
||||
tick(2000);
|
||||
expect(dialogOpenSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
flush();
|
||||
discardPeriodicTasks();
|
||||
}));
|
||||
|
||||
it('should emit downloadFileEvent when DownloadPromptDialog return DownloadPromptActions.DOWNLOAD on close', fakeAsync(() => {
|
||||
dialogOpenSpy.and.returnValue({ afterClosed: () => of(DownloadPromptActions.DOWNLOAD) } as any);
|
||||
spyOn(component.downloadFile, 'emit');
|
||||
fixture.detectChanges();
|
||||
tick(3000);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.downloadFile.emit).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
536
lib/core/viewer/src/components/viewer.component.ts
Normal file
536
lib/core/viewer/src/components/viewer.component.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
/*!
|
||||
* @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 { A11yModule } from '@angular/cdk/a11y';
|
||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ContentChild,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { filter, first, skipWhile } from 'rxjs/operators';
|
||||
import { DownloadPromptActions } from '../models/download-prompt.actions';
|
||||
import { CloseButtonPosition, Track } from '../models/viewer.model';
|
||||
import { ViewUtilService } from '../services/view-util.service';
|
||||
import { DownloadPromptDialogComponent } from './download-prompt-dialog/download-prompt-dialog.component';
|
||||
import { ViewerMoreActionsComponent } from './viewer-more-actions.component';
|
||||
import { ViewerOpenWithComponent } from './viewer-open-with.component';
|
||||
import { ViewerRenderComponent } from './viewer-render/viewer-render.component';
|
||||
import { ViewerSidebarComponent } from './viewer-sidebar.component';
|
||||
import { ViewerToolbarComponent } from './viewer-toolbar.component';
|
||||
import { ViewerToolbarActionsComponent } from './viewer-toolbar-actions.component';
|
||||
import { ViewerToolbarCustomActionsComponent } from './viewer-toolbar-custom-actions.component';
|
||||
import {
|
||||
ThumbnailService,
|
||||
IconComponent,
|
||||
ToolbarComponent,
|
||||
ToolbarDividerComponent,
|
||||
ToolbarTitleComponent,
|
||||
AppConfigService
|
||||
} from '@alfresco/adf-core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
const DEFAULT_NON_PREVIEW_CONFIG = {
|
||||
enableDownloadPrompt: false,
|
||||
enableDownloadPromptReminder: false,
|
||||
downloadPromptDelay: 50,
|
||||
downloadPromptReminderDelay: 30
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'adf-viewer',
|
||||
standalone: true,
|
||||
templateUrl: './viewer.component.html',
|
||||
styleUrls: ['./viewer.component.scss'],
|
||||
host: { class: 'adf-viewer' },
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [
|
||||
NgIf,
|
||||
A11yModule,
|
||||
ToolbarComponent,
|
||||
ToolbarTitleComponent,
|
||||
MatButtonModule,
|
||||
TranslateModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
ToolbarDividerComponent,
|
||||
ViewerRenderComponent,
|
||||
NgTemplateOutlet,
|
||||
ViewerToolbarComponent,
|
||||
ViewerSidebarComponent,
|
||||
ViewerToolbarActionsComponent,
|
||||
ViewerToolbarCustomActionsComponent,
|
||||
IconComponent
|
||||
],
|
||||
providers: [ViewUtilService]
|
||||
})
|
||||
export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
|
||||
private thumbnailService = inject(ThumbnailService);
|
||||
|
||||
@ContentChild(ViewerToolbarComponent)
|
||||
toolbar: ViewerToolbarComponent;
|
||||
|
||||
@ContentChild(ViewerSidebarComponent)
|
||||
sidebar: ViewerSidebarComponent;
|
||||
|
||||
@ContentChild(ViewerOpenWithComponent)
|
||||
mnuOpenWith: ViewerOpenWithComponent;
|
||||
|
||||
@ContentChild(ViewerMoreActionsComponent)
|
||||
mnuMoreActions: ViewerMoreActionsComponent;
|
||||
|
||||
@ContentChild('viewerExtensions', { static: false })
|
||||
viewerTemplateExtensions: TemplateRef<any>;
|
||||
|
||||
get CloseButtonPosition() {
|
||||
return CloseButtonPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Input()
|
||||
urlFile = '';
|
||||
|
||||
/** Loads a Blob File */
|
||||
@Input()
|
||||
blobFile: Blob;
|
||||
|
||||
/** Hide or show the viewer */
|
||||
@Input()
|
||||
showViewer = true;
|
||||
|
||||
/** Allows `back` navigation */
|
||||
@Input()
|
||||
allowGoBack = true;
|
||||
|
||||
/** Toggles the 'Full Screen' feature. */
|
||||
@Input()
|
||||
allowFullScreen = true;
|
||||
|
||||
/** Hide or show the toolbar */
|
||||
@Input()
|
||||
showToolbar = true;
|
||||
|
||||
/**
|
||||
* If `true` then show the Viewer as a full page over the current content.
|
||||
* Otherwise, fit inside the parent div.
|
||||
*/
|
||||
@Input()
|
||||
overlayMode = false;
|
||||
|
||||
/**
|
||||
* Toggles before/next navigation. You can use the arrow buttons to navigate
|
||||
* between documents in the collection.
|
||||
*/
|
||||
@Input()
|
||||
allowNavigate = false;
|
||||
|
||||
/** Toggles the "before" ("<") button. Requires `allowNavigate` to be enabled. */
|
||||
@Input()
|
||||
canNavigateBefore = true;
|
||||
|
||||
/** Toggles the next (">") button. Requires `allowNavigate` to be enabled. */
|
||||
@Input()
|
||||
canNavigateNext = true;
|
||||
|
||||
/** Allow the left the sidebar. */
|
||||
@Input()
|
||||
allowLeftSidebar = false;
|
||||
|
||||
/** Allow the right sidebar. */
|
||||
@Input()
|
||||
allowRightSidebar = false;
|
||||
|
||||
/** Toggles right sidebar visibility. Requires `allowRightSidebar` to be set to `true`. */
|
||||
@Input()
|
||||
showRightSidebar = false;
|
||||
|
||||
/** Toggles left sidebar visibility. Requires `allowLeftSidebar` to be set to `true`. */
|
||||
@Input()
|
||||
showLeftSidebar = false;
|
||||
|
||||
/** The template for the right sidebar. The template context contains the loaded node data. */
|
||||
@Input()
|
||||
sidebarRightTemplate: TemplateRef<any> = null;
|
||||
|
||||
/** The template for the left sidebar. The template context contains the loaded node data. */
|
||||
@Input()
|
||||
sidebarLeftTemplate: TemplateRef<any> = null;
|
||||
|
||||
/** Enable when where is possible the editing functionalities */
|
||||
@Input()
|
||||
readOnly = true;
|
||||
|
||||
/**
|
||||
* Controls which actions are enabled in the viewer.
|
||||
* Example:
|
||||
* { rotate: true, crop: false } will enable rotation but disable cropping.
|
||||
*/
|
||||
@Input()
|
||||
allowedEditActions: { [key: string]: boolean } = {
|
||||
rotate: true,
|
||||
crop: true
|
||||
};
|
||||
|
||||
/** media subtitles for the media player*/
|
||||
@Input()
|
||||
tracks: Track[] = [];
|
||||
|
||||
/** Overload mimeType*/
|
||||
@Input()
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* Context object available for binding by the local sidebarRightTemplate with let declarations.
|
||||
*/
|
||||
@Input()
|
||||
sidebarRightTemplateContext: T = null;
|
||||
|
||||
/**
|
||||
* Context object available for binding by the local sidebarLeftTemplate with let declarations.
|
||||
*/
|
||||
@Input()
|
||||
sidebarLeftTemplateContext: T = null;
|
||||
|
||||
/**
|
||||
* Change the close button position Right/Left.
|
||||
*/
|
||||
@Input()
|
||||
closeButtonPosition = CloseButtonPosition.Left;
|
||||
|
||||
/** Toggles the 'Info Button' */
|
||||
@Input()
|
||||
hideInfoButton = false;
|
||||
|
||||
/** Template containing ViewerExtensionDirective instances providing different viewer extensions based on supported file extension. */
|
||||
@Input()
|
||||
viewerExtensions: TemplateRef<any>;
|
||||
|
||||
/** Identifier of a node that is opened by the viewer. */
|
||||
@Input()
|
||||
nodeId: string = null;
|
||||
|
||||
/** Original node mime type, should be provided when renditiona mime type is different. */
|
||||
@Input()
|
||||
nodeMimeType: string = undefined;
|
||||
|
||||
/** Custom error message to be displayed in the viewer. */
|
||||
@Input()
|
||||
customError: string = undefined;
|
||||
|
||||
/**
|
||||
* Enable dialog box to allow user to download the previewed file, in case the preview is not responding for a set period of time.
|
||||
*/
|
||||
enableDownloadPrompt: boolean = false;
|
||||
|
||||
/**
|
||||
* Enable reminder dialogs to prompt user to download the file, in case the preview is not responding for a set period of time.
|
||||
*/
|
||||
enableDownloadPromptReminder: boolean = false;
|
||||
|
||||
/**
|
||||
* Initial time in seconds to wait before giving the first prompt to user to download the file
|
||||
*/
|
||||
downloadPromptDelay: number = 50;
|
||||
|
||||
/**
|
||||
* Time in seconds to wait before giving the second and consequent reminders to the user to download the file.
|
||||
*/
|
||||
downloadPromptReminderDelay: number = 15;
|
||||
|
||||
/**
|
||||
* Emitted when user clicks on download button on download prompt dialog.
|
||||
*/
|
||||
@Output()
|
||||
downloadFile = new EventEmitter<void>();
|
||||
|
||||
/** Emitted when user clicks 'Navigate Before' ("<") button. */
|
||||
@Output()
|
||||
navigateBefore = new EventEmitter<MouseEvent | KeyboardEvent>();
|
||||
|
||||
/** Emitted when user clicks 'Navigate Next' (">") button. */
|
||||
@Output()
|
||||
navigateNext = new EventEmitter<MouseEvent | KeyboardEvent>();
|
||||
|
||||
/** Emitted when the viewer close */
|
||||
@Output()
|
||||
showViewerChange = new EventEmitter<boolean>();
|
||||
|
||||
/** Emitted when the img is submitted in the img viewer. */
|
||||
@Output()
|
||||
submitFile = new EventEmitter<Blob>();
|
||||
|
||||
private closeViewer = true;
|
||||
private keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown');
|
||||
private isDialogVisible = false;
|
||||
private _fileName: string;
|
||||
private _fileNameWithoutExtension: string;
|
||||
private _fileExtension: string;
|
||||
|
||||
public downloadPromptTimer: number;
|
||||
public downloadPromptReminderTimer: number;
|
||||
public mimeTypeIconUrl: string;
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
/** Override Content filename. */
|
||||
@Input()
|
||||
set fileName(fileName: string) {
|
||||
this._fileName = fileName;
|
||||
this._fileExtension = this.viewUtilsService.getFileExtension(this.fileName);
|
||||
this._fileNameWithoutExtension = this.fileName?.replace(new RegExp(`${this.fileExtension}$`), '') || '';
|
||||
}
|
||||
|
||||
get fileName(): string {
|
||||
return this._fileName;
|
||||
}
|
||||
|
||||
get fileExtension(): string {
|
||||
return this._fileExtension;
|
||||
}
|
||||
|
||||
get fileNameWithoutExtension(): string {
|
||||
return this._fileNameWithoutExtension;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
public dialog: MatDialog,
|
||||
private viewUtilsService: ViewUtilService,
|
||||
private appConfigService: AppConfigService
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const { blobFile, urlFile, mimeType, nodeMimeType } = changes;
|
||||
|
||||
if (blobFile?.currentValue) {
|
||||
this.mimeType = blobFile.currentValue.type;
|
||||
this.mimeTypeIconUrl = this.thumbnailService.getMimeTypeIcon(blobFile.currentValue.type);
|
||||
}
|
||||
|
||||
if (urlFile?.currentValue) {
|
||||
this.fileName ||= this.viewUtilsService.getFilenameFromUrl(urlFile.currentValue);
|
||||
}
|
||||
|
||||
if (mimeType?.currentValue && !nodeMimeType?.currentValue) {
|
||||
this.mimeTypeIconUrl = this.thumbnailService.getMimeTypeIcon(mimeType.currentValue);
|
||||
}
|
||||
|
||||
if (nodeMimeType?.currentValue) {
|
||||
this.mimeTypeIconUrl = this.thumbnailService.getMimeTypeIcon(nodeMimeType.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.closeOverlayManager();
|
||||
this.configureAndInitDownloadPrompt();
|
||||
}
|
||||
|
||||
private closeOverlayManager() {
|
||||
this.dialog.afterOpened
|
||||
.pipe(
|
||||
skipWhile(() => !this.overlayMode),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(() => (this.closeViewer = false));
|
||||
|
||||
this.dialog.afterAllClosed
|
||||
.pipe(
|
||||
skipWhile(() => !this.overlayMode),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(() => (this.closeViewer = true));
|
||||
|
||||
this.keyDown$
|
||||
.pipe(
|
||||
skipWhile(() => !this.overlayMode),
|
||||
filter((e: KeyboardEvent) => e.keyCode === 27),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe((event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.closeViewer) {
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNavigateBeforeClick(event: MouseEvent | KeyboardEvent) {
|
||||
this.resetLoadingSpinner();
|
||||
this.navigateBefore.next(event);
|
||||
}
|
||||
|
||||
onNavigateNextClick(event: MouseEvent | KeyboardEvent) {
|
||||
this.resetLoadingSpinner();
|
||||
this.navigateNext.next(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* close the viewer
|
||||
*/
|
||||
onClose() {
|
||||
this.showViewer = false;
|
||||
this.showViewerChange.emit(this.showViewer);
|
||||
}
|
||||
|
||||
toggleRightSidebar() {
|
||||
this.showRightSidebar = !this.showRightSidebar;
|
||||
}
|
||||
|
||||
toggleLeftSidebar() {
|
||||
this.showLeftSidebar = !this.showLeftSidebar;
|
||||
}
|
||||
|
||||
@HostListener('document:keyup', ['$event'])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
if (event?.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowLeft' && this.canNavigateBefore) {
|
||||
event.preventDefault();
|
||||
this.onNavigateBeforeClick(event);
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight' && this.canNavigateNext) {
|
||||
event.preventDefault();
|
||||
this.onNavigateNextClick(event);
|
||||
}
|
||||
|
||||
if (event.code === 'KeyF' && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
this.enterFullScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers full screen mode with a main content area displayed.
|
||||
*/
|
||||
enterFullScreen(): void {
|
||||
if (this.allowFullScreen) {
|
||||
const container = this.el.nativeElement.querySelector('.adf-viewer__fullscreen-container');
|
||||
if (container) {
|
||||
if (container.requestFullscreen) {
|
||||
container.requestFullscreen();
|
||||
} else if (container.webkitRequestFullscreen) {
|
||||
container.webkitRequestFullscreen();
|
||||
} else if (container.mozRequestFullScreen) {
|
||||
container.mozRequestFullScreen();
|
||||
} else if (container.msRequestFullscreen) {
|
||||
container.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitFile(newImageBlob: Blob) {
|
||||
this.submitFile.emit(newImageBlob);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.clearDownloadPromptTimeouts();
|
||||
}
|
||||
|
||||
private configureAndInitDownloadPrompt() {
|
||||
this.configureDownloadPromptProperties();
|
||||
if (this.enableDownloadPrompt) {
|
||||
this.initDownloadPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private configureDownloadPromptProperties() {
|
||||
const nonResponsivePreviewConfig = this.appConfigService.get('viewer', DEFAULT_NON_PREVIEW_CONFIG);
|
||||
|
||||
this.enableDownloadPrompt = nonResponsivePreviewConfig.enableDownloadPrompt;
|
||||
this.enableDownloadPromptReminder = nonResponsivePreviewConfig.enableDownloadPromptReminder;
|
||||
this.downloadPromptDelay = nonResponsivePreviewConfig.downloadPromptDelay;
|
||||
this.downloadPromptReminderDelay = nonResponsivePreviewConfig.downloadPromptReminderDelay;
|
||||
}
|
||||
|
||||
private initDownloadPrompt() {
|
||||
this.downloadPromptTimer = window.setTimeout(() => {
|
||||
this.showOrClearDownloadPrompt();
|
||||
}, this.downloadPromptDelay * 1000);
|
||||
}
|
||||
|
||||
private showOrClearDownloadPrompt() {
|
||||
if (!this.urlFile && !this.blobFile) {
|
||||
this.showDownloadPrompt();
|
||||
} else {
|
||||
this.clearDownloadPromptTimeouts();
|
||||
}
|
||||
}
|
||||
|
||||
public clearDownloadPromptTimeouts() {
|
||||
if (this.downloadPromptTimer) {
|
||||
clearTimeout(this.downloadPromptTimer);
|
||||
}
|
||||
if (this.downloadPromptReminderTimer) {
|
||||
clearTimeout(this.downloadPromptReminderTimer);
|
||||
}
|
||||
}
|
||||
|
||||
private showDownloadPrompt() {
|
||||
if (!this.isDialogVisible) {
|
||||
this.isDialogVisible = true;
|
||||
this.dialog
|
||||
.open(DownloadPromptDialogComponent, { disableClose: true })
|
||||
.afterClosed()
|
||||
.pipe(first())
|
||||
.subscribe((result: DownloadPromptActions) => {
|
||||
this.isDialogVisible = false;
|
||||
if (result === DownloadPromptActions.DOWNLOAD) {
|
||||
this.downloadFile.emit();
|
||||
this.onClose();
|
||||
} else if (result === DownloadPromptActions.WAIT) {
|
||||
if (this.enableDownloadPromptReminder) {
|
||||
this.clearDownloadPromptTimeouts();
|
||||
this.downloadPromptReminderTimer = window.setTimeout(() => {
|
||||
this.showOrClearDownloadPrompt();
|
||||
}, this.downloadPromptReminderDelay * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private resetLoadingSpinner() {
|
||||
this.urlFile = '';
|
||||
this.blobFile = null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user