mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-06-30 18:15:11 +00:00
[ADF-1416] image viewer enhancements (#2895)
* image viewer enhancements * code improvements * fix scrolling of the zommed in content * remove flip for now as it needs much efforts * unit tests, thumbnail selector enhancements
This commit is contained in:
parent
a51d522807
commit
8a4959d172
@ -195,7 +195,10 @@
|
||||
"NEXT_PAGE": "Next page",
|
||||
"ZOOM_IN": "Zoom in",
|
||||
"ZOOM_OUT": "Zoom out",
|
||||
"FIT_PAGE": "Fit page"
|
||||
"FIT_PAGE": "Fit page",
|
||||
"ROTATE_LEFT": "Rotate left",
|
||||
"ROTATE_RIGHT": "Rotate right",
|
||||
"RESET": "Reset"
|
||||
},
|
||||
"PAGE_LABEL": {
|
||||
"SHOWING": "Showing",
|
||||
|
@ -29,6 +29,8 @@ export class ThumbnailService {
|
||||
'image/png': './assets/images/ft_ic_raster_image.svg',
|
||||
'image/jpeg': './assets/images/ft_ic_raster_image.svg',
|
||||
'image/gif': './assets/images/ft_ic_raster_image.svg',
|
||||
'application/eps': './assets/images/ft_ic_raster_image.svg',
|
||||
'application/illustrator': './assets/images/ft_ic_raster_image.svg',
|
||||
'application/pdf': './assets/images/ft_ic_pdf.svg',
|
||||
'application/vnd.ms-excel': './assets/images/ft_ic_ms_excel.svg',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': './assets/images/ft_ic_ms_excel.svg',
|
||||
|
@ -1,27 +1,56 @@
|
||||
<div class="image-container" [ngStyle]="{ transform: transform }">
|
||||
<img id="viewer-image" [src]="urlFile" [alt]="nameFile" />
|
||||
<img id="viewer-image" [src]="urlFile" [alt]="nameFile" [ngStyle]="{ 'cursor' : isDragged ? 'move': 'default' } " />
|
||||
</div>
|
||||
|
||||
<div class="adf-image-viewer__toolbar" *ngIf="showToolbar">
|
||||
<adf-toolbar>
|
||||
<button mat-icon-button (click)="zoomIn()">
|
||||
<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 mat-icon-button (click)="zoomOut()">
|
||||
<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 mat-icon-button (click)="rotateLeft()">
|
||||
<div class="adf-viewer__toolbar-page-scale">
|
||||
{{ currentScaleText }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="viewer-rotate-left-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.ROTATE_LEFT' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ROTATE_LEFT' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="rotateLeft()">
|
||||
<mat-icon>rotate_left</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button (click)="rotateRight()">
|
||||
<button
|
||||
id="viewer-rotate-right-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.ROTATE_RIGHT' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.ROTATE_RIGHT' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="rotateRight()">
|
||||
<mat-icon>rotate_right</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button (click)="flip()">
|
||||
<mat-icon>flip</mat-icon>
|
||||
<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>
|
||||
</div>
|
||||
|
@ -62,6 +62,176 @@ describe('Test Img viewer component ', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display current scale as percent string', () => {
|
||||
component.scaleX = 0.5;
|
||||
expect(component.currentScaleText).toBe('50%');
|
||||
|
||||
component.scaleX = 1.0;
|
||||
expect(component.currentScaleText).toBe('100%');
|
||||
});
|
||||
|
||||
it('should generate transform settings', () => {
|
||||
component.scaleX = 1.0;
|
||||
component.scaleY = 2.0;
|
||||
component.rotate = 10;
|
||||
component.offsetX = 20;
|
||||
component.offsetY = 30;
|
||||
|
||||
expect(component.transform).toBe('scale(1, 2) rotate(10deg) translate(20px, 30px)');
|
||||
});
|
||||
|
||||
it('should start drag on mouse down', () => {
|
||||
expect(component.isDragged).toBeFalsy();
|
||||
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
|
||||
expect(component.isDragged).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should prevent default behaviour on mouse down', () => {
|
||||
const event = jasmine.createSpyObj('mousedown', ['preventDefault']);
|
||||
|
||||
component.onMouseDown(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent default mouse move during drag', () => {
|
||||
const event = jasmine.createSpyObj('mousemove', ['preventDefault']);
|
||||
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
component.onMouseMove(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default mouse move if not dragged', () => {
|
||||
const event = jasmine.createSpyObj('mousemove', ['preventDefault']);
|
||||
|
||||
component.onMouseMove(event);
|
||||
|
||||
expect(component.isDragged).toBeFalsy();
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent default mouse up during drag end', () => {
|
||||
const event = jasmine.createSpyObj('mouseup', ['preventDefault']);
|
||||
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
expect(component.isDragged).toBeTruthy();
|
||||
|
||||
component.onMouseUp(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop drag on mouse up', () => {
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
expect(component.isDragged).toBeTruthy();
|
||||
|
||||
component.onMouseUp(<any> new CustomEvent('mouseup'));
|
||||
expect(component.isDragged).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should stop drag on mouse leave', () => {
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
expect(component.isDragged).toBeTruthy();
|
||||
|
||||
component.onMouseLeave(<any> new CustomEvent('mouseleave'));
|
||||
expect(component.isDragged).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should stop drag on mouse out', () => {
|
||||
component.onMouseDown(<any> new CustomEvent('mousedown'));
|
||||
expect(component.isDragged).toBeTruthy();
|
||||
|
||||
component.onMouseOut(<any> new CustomEvent('mouseout'));
|
||||
expect(component.isDragged).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should update scales on zoom in', () => {
|
||||
component.scaleX = 1.0;
|
||||
|
||||
component.zoomIn();
|
||||
expect(component.scaleX).toBe(1.2);
|
||||
expect(component.scaleY).toBe(1.2);
|
||||
|
||||
component.zoomIn();
|
||||
expect(component.scaleX).toBe(1.4);
|
||||
expect(component.scaleY).toBe(1.4);
|
||||
});
|
||||
|
||||
it('should update scales on zoom out', () => {
|
||||
component.scaleX = 1.0;
|
||||
|
||||
component.zoomOut();
|
||||
expect(component.scaleX).toBe(0.8);
|
||||
expect(component.scaleY).toBe(0.8);
|
||||
|
||||
component.zoomOut();
|
||||
expect(component.scaleX).toBe(0.6);
|
||||
expect(component.scaleY).toBe(0.6);
|
||||
});
|
||||
|
||||
it('should not zoom out past 20%', () => {
|
||||
component.scaleX = 0.4;
|
||||
|
||||
component.zoomOut();
|
||||
component.zoomOut();
|
||||
component.zoomOut();
|
||||
|
||||
expect(component.scaleX).toBe(0.2);
|
||||
});
|
||||
|
||||
it('should update angle by 90 degrees on rotate left', () => {
|
||||
component.rotate = 0;
|
||||
|
||||
component.rotateLeft();
|
||||
expect(component.rotate).toBe(-90);
|
||||
|
||||
component.rotateLeft();
|
||||
expect(component.rotate).toBe(-180);
|
||||
});
|
||||
|
||||
it('should reset to 0 degrees for full rotate left round', () => {
|
||||
component.rotate = -270;
|
||||
|
||||
component.rotateLeft();
|
||||
expect(component.rotate).toBe(0);
|
||||
});
|
||||
|
||||
it('should update angle by 90 degrees on rotate right', () => {
|
||||
component.rotate = 0;
|
||||
|
||||
component.rotateRight();
|
||||
expect(component.rotate).toBe(90);
|
||||
|
||||
component.rotateRight();
|
||||
expect(component.rotate).toBe(180);
|
||||
});
|
||||
|
||||
it('should reset to 0 degrees for full rotate right round', () => {
|
||||
component.rotate = 270;
|
||||
|
||||
component.rotateRight();
|
||||
expect(component.rotate).toBe(0);
|
||||
});
|
||||
|
||||
it('should reset all image modifications', () => {
|
||||
component.rotate = 10;
|
||||
component.scaleX = 20;
|
||||
component.scaleY = 30;
|
||||
component.offsetX = 40;
|
||||
component.offsetY = 50;
|
||||
|
||||
component.reset();
|
||||
|
||||
expect(component.rotate).toBe(0);
|
||||
expect(component.scaleX).toBe(1.0);
|
||||
expect(component.scaleY).toBe(1.0);
|
||||
expect(component.offsetX).toBe(0);
|
||||
expect(component.offsetY).toBe(0);
|
||||
});
|
||||
|
||||
it('If no url or blob are passed should thrown an error', () => {
|
||||
let change = new SimpleChange(null, null, true);
|
||||
expect(() => {
|
||||
|
@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation, ElementRef, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ContentService } from '../../services/content.service';
|
||||
|
||||
@Component({
|
||||
@ -25,7 +25,7 @@ import { ContentService } from '../../services/content.service';
|
||||
host: { 'class': 'adf-image-viewer' },
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ImgViewerComponent implements OnChanges {
|
||||
export class ImgViewerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input()
|
||||
showToolbar = true;
|
||||
@ -42,12 +42,94 @@ export class ImgViewerComponent implements OnChanges {
|
||||
rotate: number = 0;
|
||||
scaleX: number = 1.0;
|
||||
scaleY: number = 1.0;
|
||||
offsetX: number = 0;
|
||||
offsetY: number = 0;
|
||||
isDragged: boolean = false;
|
||||
|
||||
private drag = { x: 0, y: 0 };
|
||||
private delta = { x: 0, y: 0 };
|
||||
|
||||
get transform(): string {
|
||||
return `scale(${this.scaleX}, ${this.scaleY}) rotate(${this.rotate}deg)`
|
||||
return `scale(${this.scaleX}, ${this.scaleY}) rotate(${this.rotate}deg) translate(${this.offsetX}px, ${this.offsetY}px)`;
|
||||
}
|
||||
|
||||
constructor(private contentService: ContentService) {}
|
||||
get currentScaleText(): string {
|
||||
return Math.round(this.scaleX * 100) + '%';
|
||||
}
|
||||
|
||||
private element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private contentService: ContentService,
|
||||
private el: ElementRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.element = (<HTMLElement> this.el.nativeElement).querySelector('#viewer-image');
|
||||
|
||||
if (this.element) {
|
||||
this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.element.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.element.addEventListener('mouseleave', this.onMouseLeave.bind(this));
|
||||
this.element.addEventListener('mouseout', this.onMouseOut.bind(this));
|
||||
this.element.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.element) {
|
||||
this.element.removeEventListener('mousedown', this.onMouseDown);
|
||||
this.element.removeEventListener('mouseup', this.onMouseUp);
|
||||
this.element.removeEventListener('mouseleave', this.onMouseLeave);
|
||||
this.element.removeEventListener('mouseout', this.onMouseOut);
|
||||
this.element.removeEventListener('mousemove', this.onMouseMove);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.isDragged = true;
|
||||
this.drag = { x: event.pageX, y: event.pageY };
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (this.isDragged) {
|
||||
event.preventDefault();
|
||||
|
||||
this.delta.x = event.pageX - this.drag.x;
|
||||
this.delta.y = event.pageY - this.drag.y;
|
||||
|
||||
this.drag.x = event.pageX;
|
||||
this.drag.y = event.pageY;
|
||||
|
||||
const scaleX = (this.scaleX !== 0 ? this.scaleX : 1.0);
|
||||
const scaleY = (this.scaleY !== 0 ? this.scaleY : 1.0);
|
||||
|
||||
this.offsetX += (this.delta.x / scaleX);
|
||||
this.offsetY += (this.delta.y / scaleY);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(event: MouseEvent) {
|
||||
if (this.isDragged) {
|
||||
event.preventDefault();
|
||||
this.isDragged = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseLeave(event: MouseEvent) {
|
||||
if (this.isDragged) {
|
||||
event.preventDefault();
|
||||
this.isDragged = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseOut(event: MouseEvent) {
|
||||
if (this.isDragged) {
|
||||
event.preventDefault();
|
||||
this.isDragged = false;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
let blobFile = changes['blobFile'];
|
||||
@ -83,7 +165,11 @@ export class ImgViewerComponent implements OnChanges {
|
||||
this.rotate = Math.abs(angle) < 360 ? angle : 0;
|
||||
}
|
||||
|
||||
flip() {
|
||||
this.scaleX *= -1;
|
||||
reset() {
|
||||
this.rotate = 0;
|
||||
this.scaleX = 1.0;
|
||||
this.scaleY = 1.0;
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
<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()">
|
||||
@ -28,6 +29,7 @@
|
||||
|
||||
<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()">
|
||||
@ -44,12 +46,13 @@
|
||||
<span>{{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}</span>
|
||||
</div>
|
||||
|
||||
<div class="adf-pdf-viewer__toolbar-page-scale">
|
||||
<div class="adf-viewer__toolbar-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()">
|
||||
@ -58,6 +61,7 @@
|
||||
|
||||
<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()">
|
||||
@ -66,6 +70,7 @@
|
||||
|
||||
<button
|
||||
id="viewer-scale-page-button"
|
||||
title="{{ 'ADF_VIEWER.ARIA.FIT_PAGE' | translate }}"
|
||||
attr.aria-label="{{ 'ADF_VIEWER.ARIA.FIT_PAGE' | translate }}"
|
||||
mat-icon-button
|
||||
(click)="pageFit()">
|
||||
|
@ -53,18 +53,6 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@
|
||||
.adf-viewer-layout-content {
|
||||
@extend .full-screen;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
z-index: 1;
|
||||
background-color: mat-color($background, background);
|
||||
@ -143,5 +143,19 @@
|
||||
border-left: 1px solid mat-color($foreground, text, 0.07);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user