mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[ACS-10884] Keyboard interactions for crop tool in image viewer (#11522)
* [ACS-10884] Keyboard interactions for crop tool in image viewer * [ACS-10884] Sonarqube fix * [ACS-10884] MD formatting fix
This commit is contained in:
@@ -33,6 +33,26 @@ describe('Test Img viewer component ', () => {
|
||||
return new Blob([data], { type: 'image/png' });
|
||||
};
|
||||
|
||||
const dispatchKeyboardEvent = (key: string, shiftKey = false, altKey = false): KeyboardEvent => {
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
key,
|
||||
shiftKey,
|
||||
altKey
|
||||
});
|
||||
spyOn(event, 'preventDefault');
|
||||
document.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
const getExpectedCropBoxData = (cropper: Cropper, left: number, width: number, top: number, height: number): Cropper.CropBoxData => {
|
||||
const cropBoxData = cropper.getCropBoxData();
|
||||
cropBoxData.left += left;
|
||||
cropBoxData.top += top;
|
||||
cropBoxData.width += width;
|
||||
cropBoxData.height += height;
|
||||
return cropBoxData;
|
||||
};
|
||||
|
||||
describe('Zoom customization', () => {
|
||||
beforeEach(() => {
|
||||
urlService = TestBed.inject(UrlService);
|
||||
@@ -375,4 +395,134 @@ describe('Test Img viewer component ', () => {
|
||||
expect(testingUtils.getByCSS('#viewer-crop-button')).toBeNull('Crop button should not be visible when disallowed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard interactions', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImgViewerComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should rotate the image when r key is pressed', () => {
|
||||
spyOn(component, 'rotateImage');
|
||||
dispatchKeyboardEvent('r');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.rotateImage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should zoom the image out when o key is pressed', () => {
|
||||
spyOn(component, 'zoomOut');
|
||||
dispatchKeyboardEvent('o');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.zoomOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should zoom the image in when i key is pressed', () => {
|
||||
spyOn(component, 'zoomIn');
|
||||
dispatchKeyboardEvent('i');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.zoomIn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should move the cropper when arrow keys are pressed', () => {
|
||||
spyOn(component.cropper, 'move');
|
||||
dispatchKeyboardEvent('ArrowLeft');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.move).toHaveBeenCalledWith(-3, 0);
|
||||
|
||||
dispatchKeyboardEvent('ArrowRight');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.move).toHaveBeenCalledWith(3, 0);
|
||||
|
||||
dispatchKeyboardEvent('ArrowUp');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.move).toHaveBeenCalledWith(0, -3);
|
||||
|
||||
dispatchKeyboardEvent('ArrowDown');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.move).toHaveBeenCalledWith(0, 3);
|
||||
});
|
||||
|
||||
it('should increase crop box area when arrow keys with shift are pressed', () => {
|
||||
component.cropImage();
|
||||
spyOn(component.cropper, 'setCropBoxData');
|
||||
let expectedCropBoxData = getExpectedCropBoxData(component.cropper, -3, 3, 0, 0);
|
||||
dispatchKeyboardEvent('ArrowLeft', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 3, 0, 0);
|
||||
dispatchKeyboardEvent('ArrowRight', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, -3, 3);
|
||||
dispatchKeyboardEvent('ArrowUp', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 0, 3);
|
||||
dispatchKeyboardEvent('ArrowDown', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
});
|
||||
|
||||
it('should decrease crop box area when arrow keys with alt are pressed', () => {
|
||||
component.cropImage();
|
||||
spyOn(component.cropper, 'setCropBoxData');
|
||||
let expectedCropBoxData = getExpectedCropBoxData(component.cropper, 3, -3, 0, 0);
|
||||
dispatchKeyboardEvent('ArrowLeft', false, true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, -3, 0, 0);
|
||||
dispatchKeyboardEvent('ArrowRight', false, true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 3, -3);
|
||||
dispatchKeyboardEvent('ArrowUp', false, true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
|
||||
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 0, -3);
|
||||
dispatchKeyboardEvent('ArrowDown', false, true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
|
||||
});
|
||||
|
||||
it('should prevent default for all arrow keys events', () => {
|
||||
const leftEvent = dispatchKeyboardEvent('ArrowLeft');
|
||||
fixture.detectChanges();
|
||||
expect(leftEvent.preventDefault).toHaveBeenCalled();
|
||||
|
||||
const rightEvent = dispatchKeyboardEvent('ArrowRight');
|
||||
fixture.detectChanges();
|
||||
expect(rightEvent.preventDefault).toHaveBeenCalled();
|
||||
|
||||
const upEvent = dispatchKeyboardEvent('ArrowUp');
|
||||
fixture.detectChanges();
|
||||
expect(upEvent.preventDefault).toHaveBeenCalled();
|
||||
|
||||
const downEvent = dispatchKeyboardEvent('ArrowDown');
|
||||
fixture.detectChanges();
|
||||
expect(downEvent.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,27 +85,23 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
@ViewChild('image', { static: false })
|
||||
imageElement: ElementRef;
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
@HostListener('document:keyup', ['$event'])
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft': {
|
||||
event.preventDefault();
|
||||
this.cropper.move(-3, 0);
|
||||
this.handleArrowLeftKey(event);
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
event.preventDefault();
|
||||
this.cropper.move(0, -3);
|
||||
this.handleArrowUpKey(event);
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
event.preventDefault();
|
||||
this.cropper.move(3, 0);
|
||||
this.handleArrowRightKey(event);
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
event.preventDefault();
|
||||
this.cropper.move(0, 3);
|
||||
this.handleArrowDownKey(event);
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
@@ -255,4 +251,57 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
onImageError() {
|
||||
this.error.emit();
|
||||
}
|
||||
|
||||
private handleArrowLeftKey(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.changeCropBoxArea(-3, 3, 0, 0);
|
||||
} else if (event.altKey) {
|
||||
this.changeCropBoxArea(3, -3, 0, 0);
|
||||
} else {
|
||||
this.cropper.move(-3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private handleArrowUpKey(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.changeCropBoxArea(0, 0, -3, 3);
|
||||
} else if (event.altKey) {
|
||||
this.changeCropBoxArea(0, 0, 3, -3);
|
||||
} else {
|
||||
this.cropper.move(0, -3);
|
||||
}
|
||||
}
|
||||
|
||||
private handleArrowRightKey(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.changeCropBoxArea(0, 3, 0, 0);
|
||||
} else if (event.altKey) {
|
||||
this.changeCropBoxArea(0, -3, 0, 0);
|
||||
} else {
|
||||
this.cropper.move(3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private handleArrowDownKey(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.changeCropBoxArea(0, 0, 0, 3);
|
||||
} else if (event.altKey) {
|
||||
this.changeCropBoxArea(0, 0, 0, -3);
|
||||
} else {
|
||||
this.cropper.move(0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private changeCropBoxArea(left: number, width: number, top: number, height: number) {
|
||||
const cropBoxData = this.cropper.getCropBoxData();
|
||||
cropBoxData.left += left;
|
||||
cropBoxData.width += width;
|
||||
cropBoxData.top += top;
|
||||
cropBoxData.height += height;
|
||||
this.cropper.setCropBoxData(cropBoxData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import { AppExtensionService, ExtensionsModule, ViewerExtensionRef, PreviewExtensionComponent } from '@alfresco/adf-extensions';
|
||||
import { NgForOf, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, EventEmitter, Injector, Input, OnChanges, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, EventEmitter, Injector, Input, OnChanges, OnInit, Output, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
@@ -129,6 +129,9 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
|
||||
@Output()
|
||||
isSaving = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(ImgViewerComponent)
|
||||
imgViewer: ImgViewerComponent;
|
||||
|
||||
extensionTemplates: { template: TemplateRef<any>; isVisible: boolean }[] = [];
|
||||
extensionsSupportedByTemplates: string[] = [];
|
||||
extension: string;
|
||||
|
||||
@@ -53,6 +53,20 @@ describe('ViewerComponent', () => {
|
||||
const getFileName = (): string => testingUtils.getByCSS('#adf-viewer-display-name').nativeElement.textContent;
|
||||
const getTitle = (): string => testingUtils.getByCSS('.adf-viewer__title-value')?.nativeElement?.textContent;
|
||||
const getDividers = (): DebugElement[] => testingUtils.getAllByCSS('.adf-toolbar-divider');
|
||||
const mockImgViewerEditMode = (isEditing: boolean) => {
|
||||
const imgViewerMock = jasmine.createSpyObj('ImgViewerComponent', [], {
|
||||
isEditing
|
||||
});
|
||||
component.viewerRenderer.imgViewer = imgViewerMock;
|
||||
};
|
||||
const dispatchKeyboardEvent = (key: string): KeyboardEvent => {
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
key
|
||||
});
|
||||
spyOn(event, 'preventDefault');
|
||||
document.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -750,4 +764,79 @@ describe('ViewerComponent', () => {
|
||||
expect(component.downloadFile.emit).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('keyboard events', () => {
|
||||
it('should do nothing when default was already prevented', () => {
|
||||
spyOn(component, 'onNavigateBeforeClick').and.callThrough();
|
||||
spyOn(component, 'onNavigateNextClick').and.callThrough();
|
||||
spyOn(component, 'enterFullScreen').and.callThrough();
|
||||
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
key: 'a'
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
document.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.onNavigateBeforeClick).not.toHaveBeenCalled();
|
||||
expect(component.onNavigateNextClick).not.toHaveBeenCalled();
|
||||
expect(component.enterFullScreen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enter full screen mode when ctrl + F is pressed', () => {
|
||||
spyOn(component, 'enterFullScreen').and.callThrough();
|
||||
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
code: 'KeyF',
|
||||
ctrlKey: true
|
||||
});
|
||||
spyOn(event, 'preventDefault');
|
||||
document.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(component.enterFullScreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate before when left arrow is pressed', () => {
|
||||
component.canNavigateBefore = true;
|
||||
spyOn(component, 'onNavigateBeforeClick').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
mockImgViewerEditMode(true);
|
||||
const event = dispatchKeyboardEvent('ArrowLeft');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(component.onNavigateBeforeClick).not.toHaveBeenCalled();
|
||||
|
||||
mockImgViewerEditMode(false);
|
||||
const event2 = dispatchKeyboardEvent('ArrowLeft');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event2.preventDefault).toHaveBeenCalled();
|
||||
expect(component.onNavigateBeforeClick).toHaveBeenCalledWith(event2);
|
||||
});
|
||||
|
||||
it('should navigate next when right arrow is pressed', () => {
|
||||
component.canNavigateNext = true;
|
||||
spyOn(component, 'onNavigateNextClick').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
mockImgViewerEditMode(true);
|
||||
const event = dispatchKeyboardEvent('ArrowRight');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(component.onNavigateNextClick).not.toHaveBeenCalled();
|
||||
|
||||
mockImgViewerEditMode(false);
|
||||
const event2 = dispatchKeyboardEvent('ArrowRight');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event2.preventDefault).toHaveBeenCalled();
|
||||
expect(component.onNavigateNextClick).toHaveBeenCalledWith(event2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
Output,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -299,6 +300,9 @@ export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
|
||||
@Output()
|
||||
submitFile = new EventEmitter<Blob>();
|
||||
|
||||
@ViewChild(ViewerRenderComponent)
|
||||
viewerRenderer: ViewerRenderComponent;
|
||||
|
||||
private closeViewer = true;
|
||||
private keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown');
|
||||
private isDialogVisible = false;
|
||||
@@ -437,14 +441,16 @@ export class ViewerComponent<T> implements OnDestroy, OnInit, OnChanges {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowLeft' && this.canNavigateBefore) {
|
||||
event.preventDefault();
|
||||
this.onNavigateBeforeClick(event);
|
||||
}
|
||||
if (!this.viewerRenderer?.imgViewer?.isEditing) {
|
||||
if (event.key === 'ArrowLeft' && this.canNavigateBefore) {
|
||||
event.preventDefault();
|
||||
this.onNavigateBeforeClick(event);
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight' && this.canNavigateNext) {
|
||||
event.preventDefault();
|
||||
this.onNavigateNextClick(event);
|
||||
if (event.key === 'ArrowRight' && this.canNavigateNext) {
|
||||
event.preventDefault();
|
||||
this.onNavigateNextClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.code === 'KeyF' && event.ctrlKey) {
|
||||
|
||||
Reference in New Issue
Block a user