[ADF-1412] Viewer enhancements (#2873)

* collection navigation support for Viewer

* code cleanup

* full screen mode support

* keyboard shortcuts

* zooming scale label

* layout fixes

* test fixes

* image toolbar with basic tranformations

* test fixes

* test fixes
This commit is contained in:
Denys Vuika 2018-01-28 23:01:01 +00:00 committed by Eugenio Romano
parent d2d635b94d
commit 08f2cc9236
16 changed files with 358 additions and 45 deletions

View File

@ -65,6 +65,10 @@ Using with file url:
| showSidebar | boolean | false | Toggles sidebar visibility. Requires `allowSidebar` to be set to `true`. |
| sidebarPosition | string | right | The position of the sidebar. Can be `left` or `right`. |
| sidebarTemplate | TemplateRef<any> | null | The template intended to be used as a sidebar. The template context contains the loaded node data. |
| allowNavigate | boolean | false | Toggle before/next navigation, arrow buttons to navigate between multiple documents in the collection. |
| canNavigateBefore | boolean | true | Toggle the "before" ("<") button. Requires `allowNavigate` to be enabled. |
| canNavigateNext | boolean | true | Toggle the next (">") button. Requires `allowNavigate` to be enabled.|
| allowFullScreen | boolean | true | Toggle the 'Full Screen' feature. |
### Events
@ -74,6 +78,17 @@ Using with file url:
| download | any | Yes | Raised when user clicks the 'Download' button. |
| print | any | Yes | Raised when user clicks the 'Print' button. |
| share | any | Yes | Raised when user clicks the 'Share' button. |
| navigateBefore | any | No | Raised when user clicks 'Navigate Before' ("<") button. |
| navigateNext | any | No | Raised when user clicks 'Navigate Next' (">") button. |
### Keyboard shortcuts
| Name | Description |
| --- | --- |
| Esc | Close the viewer (overlay mode only). |
| Left | Invoke 'Navigate before' action. |
| Right | Invoke 'Navigate next' action. |
| Ctrl+F | Activate full-screen mode. |
## Details

View File

@ -187,7 +187,8 @@
"PRINT": "Print",
"SHARE": "Share",
"MORE_ACTIONS": "More actions",
"INFO": "Info"
"INFO": "Info",
"FULLSCREEN": "Activate full-screen mode"
},
"ARIA": {
"PREVIOUS_PAGE": "Previous page",

View File

@ -24,6 +24,13 @@ export class EventMock {
document.dispatchEvent(event);
}
static keyUp(key: any) {
let event: any = document.createEvent('Event');
event.keyCode = key;
event.initEvent('keyup');
document.dispatchEvent(event);
}
static resizeMobileView() {
// todo: no longer compiles with TS 2.0.2 as innerWidth/innerHeight are readonly fields
/*

View File

@ -17,9 +17,11 @@
@import '../toolbar/toolbar.component';
@import '../userinfo/components/user-info.component';
@import '../viewer/components/viewer.component';
@import '../viewer/components/pdfViewer.component';
@import '../viewer/components/txtViewer.component';
@import '../viewer/components/imgViewer.component';
@import '../form/components/form.component';
@import '../sidebar/sidebar-action-menu.component';
@import '../viewer/components/pdfViewer.component';
@mixin adf-core-theme($theme) {
@include adf-colors-theme($theme);
@ -40,6 +42,8 @@
@include adf-userinfo-theme($theme);
@include adf-viewer-theme($theme);
@include adf-pdf-viewer-theme($theme);
@include adf-image-viewer-theme($theme);
@include adf-text-viewer-theme($theme);
@include adf-form-component-theme($theme);
@include adf-sidebar-action-menu-theme($theme);
}

View File

@ -1,3 +1,27 @@
<div class="image-container">
<div class="image-container" [ngStyle]="{ transform: transform }">
<img id="viewer-image" [src]="urlFile" [alt]="nameFile" />
</div>
<div class="adf-image-viewer__toolbar" *ngIf="showToolbar">
<adf-toolbar>
<button mat-icon-button (click)="zoomIn()">
<mat-icon>zoom_in</mat-icon>
</button>
<button mat-icon-button (click)="zoomOut()">
<mat-icon>zoom_out</mat-icon>
</button>
<button mat-icon-button (click)="rotateLeft()">
<mat-icon>rotate_left</mat-icon>
</button>
<button mat-icon-button (click)="rotateRight()">
<mat-icon>rotate_right</mat-icon>
</button>
<button mat-icon-button (click)="flip()">
<mat-icon>flip</mat-icon>
</button>
</adf-toolbar>
</div>

View File

@ -1,14 +1,33 @@
.adf-img-viewer {
.image-container {
display: flex;
flex: 1;
text-align: center;
flex-direction: row;
justify-content: center;
height: 90vh;
img {
width: 100%;
object-fit: contain;
@mixin adf-image-viewer-theme($theme) {
.adf-image-viewer {
.image-container {
display: flex;
flex: 1;
text-align: center;
flex-direction: row;
justify-content: center;
height: 90vh;
img {
width: 100%;
object-fit: contain;
}
}
&__toolbar {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
.adf-toolbar .mat-toolbar {
max-height: 48px;
background-color: mat-color($primary, default-contrast, 1);
border-width: 0;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12);
}
}
}
}

View File

@ -21,6 +21,8 @@ import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { AuthenticationService } from '../../services/authentication.service';
import { ContentService } from '../../services/content.service';
import { SettingsService } from '../../services/settings.service';
import { MaterialModule } from '../../material.module';
import { ToolbarModule } from '../../toolbar/toolbar.module';
import { ImgViewerComponent } from './imgViewer.component';
@ -38,7 +40,10 @@ describe('Test Img viewer component ', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
ToolbarModule
],
declarations: [ImgViewerComponent],
providers: [
SettingsService,

View File

@ -22,11 +22,14 @@ import { ContentService } from '../../services/content.service';
selector: 'adf-img-viewer',
templateUrl: './imgViewer.component.html',
styleUrls: ['./imgViewer.component.scss'],
host: { 'class': 'adf-img-viewer' },
host: { 'class': 'adf-image-viewer' },
encapsulation: ViewEncapsulation.None
})
export class ImgViewerComponent implements OnChanges {
@Input()
showToolbar = true;
@Input()
urlFile: string;
@ -36,6 +39,14 @@ export class ImgViewerComponent implements OnChanges {
@Input()
nameFile: string;
rotate: number = 0;
scaleX: number = 1.0;
scaleY: number = 1.0;
get transform(): string {
return `scale(${this.scaleX}, ${this.scaleY}) rotate(${this.rotate}deg)`
}
constructor(private contentService: ContentService) {}
ngOnChanges(changes: SimpleChanges) {
@ -48,4 +59,31 @@ export class ImgViewerComponent implements OnChanges {
throw new Error('Attribute urlFile or blobFile is required');
}
}
zoomIn() {
const ratio = +((this.scaleX + 0.2).toFixed(1));
this.scaleX = this.scaleY = ratio;
}
zoomOut() {
let ratio = +((this.scaleX - 0.2).toFixed(1));
if (ratio < 0.2) {
ratio = 0.2;
}
this.scaleX = this.scaleY = ratio;
}
rotateLeft() {
const angle = this.rotate - 90;
this.rotate = Math.abs(angle) < 360 ? angle : 0;
}
rotateRight() {
const angle = this.rotate + 90;
this.rotate = Math.abs(angle) < 360 ? angle : 0;
}
flip() {
this.scaleX *= -1;
}
}

View File

@ -44,7 +44,9 @@
<span>{{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}</span>
</div>
<adf-toolbar-divider></adf-toolbar-divider>
<div class="adf-pdf-viewer__toolbar-page-scale">
{{ currentScaleText }}
</div>
<button
id="viewer-zoom-in-button"

View File

@ -1,4 +1,6 @@
@mixin adf-pdf-viewer-theme($theme) {
$foreground: map-get($theme, foreground);
.adf-pdf-viewer {
.loader-container {
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
@ -39,8 +41,11 @@
font-size: 14px;
& > input {
border: 1px solid mat-color($foreground, text, 0.07);
font-size: 14px;
padding: 4px 0;
padding: 0;
height: 24px;
line-height: 24px;
text-align: right;
width: 33px;
margin-right: 4px;
@ -48,6 +53,18 @@
outline-color: gray;
}
}
&-page-scale {
cursor: default;
width: 79px;
height: 24px;
font-size: 14px;
border: 1px solid mat-color($foreground, text, 0.07);
text-align: center;
line-height: 24px;
margin-left: 4px;
margin-right: 4px;
}
}
}
}

View File

@ -56,13 +56,17 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
loadingPercent: number;
pdfViewer: any;
currentScaleMode: string = 'auto';
currentScale: number;
currentScale: number = 1;
MAX_AUTO_SCALE: number = 1.25;
DEFAULT_SCALE_DELTA: number = 1.1;
MIN_SCALE: number = 0.25;
MAX_SCALE: number = 10.0;
get currentScaleText(): string {
return Math.round(this.currentScale * 100) + '%';
}
constructor(private renderingQueueServices: RenderingQueueServices,
private logService: LogService) {
// needed to preserve "this" context when setting as a global document event listener
@ -163,21 +167,21 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
let documentContainer = document.getElementById('viewer-pdf-container');
let widthContainer;
let heigthContainer;
let heightContainer;
if (viewerContainer && viewerContainer.clientWidth <= documentContainer.clientWidth) {
widthContainer = viewerContainer.clientWidth;
heigthContainer = viewerContainer.clientHeight;
heightContainer = viewerContainer.clientHeight;
} else {
widthContainer = documentContainer.clientWidth;
heigthContainer = documentContainer.clientHeight;
heightContainer = documentContainer.clientHeight;
}
let currentPage = this.pdfViewer._pages[this.pdfViewer._currentPageNumber - 1];
let padding = 20;
let pageWidthScale = (widthContainer - padding) / currentPage.width * currentPage.scale;
let pageHeightScale = (heigthContainer - padding) / currentPage.width * currentPage.scale;
let pageHeightScale = (heightContainer - padding) / currentPage.width * currentPage.scale;
let scale;
@ -231,7 +235,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
}
/**
* method to check if the request scale of the page is the same for avoid unuseful re-rendering
* Check if the request scale of the page is the same for avoid useless re-rendering
*
* @param {number} oldScale - old scale page
* @param {number} newScale - new scale page
@ -243,7 +247,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
}
/**
* method to check if is a land scape view
* Check if is a land scape view
*
* @param {number} width
* @param {number} height

View File

@ -1,6 +1,9 @@
.adf-txt-viewer {
background-color: white;
width: 100vw;
overflow: hidden;
overflow-y: scroll;
@mixin adf-text-viewer-theme($theme) {
$background: map-get($theme, background);
.adf-txt-viewer {
background-color: mat-color($background, background);
overflow: hidden;
overflow-y: scroll;
}
}

View File

@ -63,6 +63,15 @@
<mat-icon>share</mat-icon>
</button>
<button
*ngIf="allowFullScreen"
mat-icon-button
matTooltip="{{ 'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate }}"
data-automation-id="toolbar-fullscreen"
(click)="enterFullScreen()">
<mat-icon>fullscreen</mat-icon>
</button>
<ng-container *ngIf="mnuMoreActions">
<button
mat-icon-button
@ -104,7 +113,7 @@
<div *ngIf="!isLoading" fxLayout="row" fxFlex="1 1 auto">
<ng-container *ngIf="allowSidebar && showSidebar">
<div class="adf-viewer__sidebar" fxFlexOrder="{{sidebarPosition === 'left'? 1: 2 }}">
<div class="adf-viewer__sidebar" fxFlexOrder="{{sidebarPosition === 'left'? 1 : 4 }}">
<ng-container *ngIf="sidebarTemplate">
<ng-container *ngTemplateOutlet="sidebarTemplate;context:sidebarTemplateContext"></ng-container>
</ng-container>
@ -112,10 +121,17 @@
</div>
</ng-container>
<div fxFlexOrder="{{sidebarPosition !== 'left'? 1: 2}}" fxFlex="1 1 auto">
<div class="adf-viewer-layout-content">
<div class="adf-viewer-content-container" [ngSwitch]="viewerType">
<ng-container *ngIf="allowNavigate && canNavigateBefore">
<div class="navigate-before">
<button mat-mini-fab color="primary" (click)="onNavigateBeforeClick()">
<mat-icon>navigate_before</mat-icon>
</button>
</div>
</ng-container>
<div class="adf-viewer-main" fxFlexOrder="{{sidebarPosition !== 'left'? 1 : 4}}" fxFlex="1 1 auto">
<div class="adf-viewer-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-content-container" [ngSwitch]="viewerType">
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer [blobFile]="blobFile" [urlFile]="urlFileContent" [nameFile]="displayName"></adf-pdf-viewer>
</ng-container>
@ -142,10 +158,17 @@
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format></adf-viewer-unknown-format>
</ng-container>
</div>
</div>
</div>
<ng-container *ngIf="allowNavigate && canNavigateNext">
<div class="navigate-next" fxFlexOrder="{{sidebarPosition !== 'left' ? 3 : 4}}">
<button mat-mini-fab color="primary" (click)="onNavigateNextClick()">
<mat-icon>navigate_next</mat-icon>
</button>
</div>
</ng-container>
</div>
</div>
</div>

View File

@ -11,6 +11,30 @@
.adf-viewer {
.navigate-before {
display: flex;
align-items: center;
justify-content: center;
order: 1;
padding-left: 2px;
padding-right: 4px;
background-color: mat-color($background, background);
}
&-main {
width: 0;
}
.navigate-next {
display: flex;
align-items: center;
justify-content: center;
order: 3;
padding-left: 4px;
padding-right: 2px;
background-color: mat-color($background, background);
}
&__mimeicon {
vertical-align: middle;
height: 18px;
@ -19,7 +43,7 @@
&-toolbar {
.mat-toolbar {
background-color: mat-color($primary, default-contrast)
background-color: mat-color($primary, default-contrast);
}
}

View File

@ -232,7 +232,7 @@ describe('ViewerComponent', () => {
component.sidebarPosition = 'right';
fixture.detectChanges();
let sidebar = element.querySelector('.adf-viewer__sidebar');
expect(getComputedStyle(sidebar).order).toEqual('2');
expect(getComputedStyle(sidebar).order).toEqual('4');
});
it('should display sidebar on the right side as fallback', () => {
@ -241,11 +241,71 @@ describe('ViewerComponent', () => {
component.sidebarPosition = 'unknown-value';
fixture.detectChanges();
let sidebar = element.querySelector('.adf-viewer__sidebar');
expect(getComputedStyle(sidebar).order).toEqual('2');
expect(getComputedStyle(sidebar).order).toEqual('4');
});
describe('Full Screen Mode', () => {
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('Toolbar', () => {
it('should render fullscreen button', () => {
component.allowFullScreen = true;
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="toolbar-fullscreen"]')).toBeDefined();
});
it('should not render fullscreen button', () => {
component.allowFullScreen = false;
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="toolbar-fullscreen"]')).toBeNull();
});
it('should render default download button', () => {
component.allowDownload = true;
fixture.detectChanges();
@ -397,7 +457,7 @@ describe('ViewerComponent', () => {
});
it('should Esc button hide the viewer', () => {
EventMock.keyDown(27);
EventMock.keyUp(27);
fixture.detectChanges();
expect(element.querySelector('.adf-viewer-content')).toBeNull();
});

View File

@ -17,7 +17,7 @@
import { Location } from '@angular/common';
import {
Component, ContentChild, EventEmitter, HostListener,
Component, ContentChild, EventEmitter, HostListener, ElementRef,
Input, OnChanges, Output, SimpleChanges, TemplateRef, ViewEncapsulation
} from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@ -90,6 +90,18 @@ export class ViewerComponent implements OnChanges {
@Input()
allowShare = false;
@Input()
allowFullScreen = true;
@Input()
allowNavigate = false;
@Input()
canNavigateBefore = true;
@Input()
canNavigateNext = true;
@Input()
allowSidebar = false;
@ -129,6 +141,12 @@ export class ViewerComponent implements OnChanges {
@Output()
extensionChange = new EventEmitter<string>();
@Output()
navigateBefore = new EventEmitter();
@Output()
navigateNext = new EventEmitter();
viewerType = 'unknown';
isLoading = false;
node: MinimalNodeEntryEntity;
@ -143,7 +161,7 @@ export class ViewerComponent implements OnChanges {
private extensions = {
image: ['png', 'jpg', 'jpeg', 'gif', 'bpm'],
media: ['wav', 'mp4', 'mp3', 'webm', 'ogg'],
text: ['txt', 'xml', 'js', 'html', 'json'],
text: ['txt', 'xml', 'js', 'html', 'json', 'ts'],
pdf: ['pdf']
};
@ -155,7 +173,8 @@ export class ViewerComponent implements OnChanges {
constructor(private apiService: AlfrescoApiService,
private logService: LogService,
private location: Location,
private renditionService: RenditionsService) {
private renditionService: RenditionsService,
private el: ElementRef) {
}
isSourceDefined(): boolean {
@ -354,6 +373,14 @@ export class ViewerComponent implements OnChanges {
}
}
onNavigateBeforeClick() {
this.navigateBefore.next();
}
onNavigateNextClick() {
this.navigateNext.next();
}
/**
* close the viewer
*/
@ -409,15 +436,35 @@ export class ViewerComponent implements OnChanges {
}
/**
* Litener Keyboard Event
* Keyboard event listener
* @param {KeyboardEvent} event
*/
@HostListener('document:keydown', ['$event'])
@HostListener('document:keyup', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
let key = event.keyCode;
const key = event.keyCode;
// Esc
if (key === 27 && this.overlayMode) { // esc
this.close();
}
// Left arrow
if (key === 37 && this.canNavigateBefore) {
event.preventDefault();
this.onNavigateBeforeClick();
}
// Right arrow
if (key === 39 && this.canNavigateNext) {
event.preventDefault();
this.onNavigateNextClick();
}
// Ctrl+F
if (key === 70 && event.ctrlKey) {
event.preventDefault();
this.enterFullScreen();
}
}
downloadContent() {
@ -453,6 +500,26 @@ export class ViewerComponent implements OnChanges {
}
}
/**
* 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();
}
}
}
}
private async displayNodeRendition(nodeId: string) {
this.isLoading = true;