Skip to content

Commit

Permalink
feat(module:image): zoom using mouse wheel (#8180)
Browse files Browse the repository at this point in the history
  • Loading branch information
ParsaArvanehPA committed Feb 19, 2024
1 parent e856515 commit 4235c29
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 15 deletions.
87 changes: 77 additions & 10 deletions components/image/image-preview.component.ts
Expand Up @@ -4,7 +4,7 @@
*/

import { AnimationEvent } from '@angular/animations';
import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
import { CdkDrag, CdkDragEnd, CdkDragHandle } from '@angular/cdk/drag-drop';
import { OverlayRef } from '@angular/cdk/overlay';
import {
ChangeDetectionStrategy,
Expand Down Expand Up @@ -83,7 +83,7 @@ const NZ_DEFAULT_ROTATE = 0;
cdkDrag
[style.transform]="previewImageWrapperTransform"
[cdkDragFreeDragPosition]="position"
(cdkDragReleased)="onDragReleased()"
(cdkDragEnded)="onDragEnd($event)"
>
@for (image of images; track image; let imageIndex = $index) {
@if (imageIndex === index) {
Expand Down Expand Up @@ -268,18 +268,24 @@ export class NzImagePreviewComponent implements OnInit {
.subscribe(() => {
this.isDragging = true;
});

fromEvent<WheelEvent>(this.imagePreviewWrapper.nativeElement, 'wheel')
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
this.ngZone.run(() => this.wheelZoomEventHandler(event));
});
});
}

setImages(images: NzImage[], scaleStepMap?: Map<string, number>): void {
if (scaleStepMap) this.scaleStepMap = scaleStepMap;
this.images = images;
this.cdr.markForCheck();
this.markForCheck();
}

switchTo(index: number): void {
this.index = index;
this.cdr.markForCheck();
this.markForCheck();
}

next(): void {
Expand All @@ -289,7 +295,7 @@ export class NzImagePreviewComponent implements OnInit {
this.updatePreviewImageTransform();
this.updatePreviewImageWrapperTransform();
this.updateZoomOutDisabled();
this.cdr.markForCheck();
this.markForCheck();
}
}

Expand All @@ -300,7 +306,7 @@ export class NzImagePreviewComponent implements OnInit {
this.updatePreviewImageTransform();
this.updatePreviewImageWrapperTransform();
this.updateZoomOutDisabled();
this.cdr.markForCheck();
this.markForCheck();
}
}

Expand All @@ -318,7 +324,6 @@ export class NzImagePreviewComponent implements OnInit {
this.zoom += zoomStep;
this.updatePreviewImageTransform();
this.updateZoomOutDisabled();
this.position = { ...initialPosition };
}

onZoomOut(): void {
Expand All @@ -328,7 +333,10 @@ export class NzImagePreviewComponent implements OnInit {
this.zoom -= zoomStep;
this.updatePreviewImageTransform();
this.updateZoomOutDisabled();
this.position = { ...initialPosition };

if (this.zoom <= 1) {
this.reCenterImage();
}
}
}

Expand Down Expand Up @@ -364,6 +372,19 @@ export class NzImagePreviewComponent implements OnInit {
this.updatePreviewImageTransform();
}

wheelZoomEventHandler(event: WheelEvent): void {
event.preventDefault();
event.stopPropagation();

this.handlerImageTransformationWhileZoomingWithMouse(event, event.deltaY);
this.handleImageScaleWhileZoomingWithMouse(event.deltaY);

this.updatePreviewImageWrapperTransform();
this.updatePreviewImageTransform();

this.markForCheck();
}

onAnimationStart(event: AnimationEvent): void {
if (event.toState === 'enter') {
this.setEnterAnimationClass();
Expand All @@ -385,10 +406,10 @@ export class NzImagePreviewComponent implements OnInit {

startLeaveAnimation(): void {
this.animationState = 'leave';
this.cdr.markForCheck();
this.markForCheck();
}

onDragReleased(): void {
onDragEnd(event: CdkDragEnd): void {
this.isDragging = false;
const width = this.imageRef.nativeElement.offsetWidth * this.zoom;
const height = this.imageRef.nativeElement.offsetHeight * this.zoom;
Expand All @@ -406,6 +427,11 @@ export class NzImagePreviewComponent implements OnInit {
const fitContentPos = getFitContentPosition(fitContentParams);
if (isNotNil(fitContentPos.x) || isNotNil(fitContentPos.y)) {
this.position = { ...this.position, ...fitContentPos };
} else if (!isNotNil(fitContentPos.x) && !isNotNil(fitContentPos.y)) {
this.position = {
x: event.source.getFreeDragPosition().x,
y: event.source.getFreeDragPosition().y
};
}
}

Expand Down Expand Up @@ -449,12 +475,53 @@ export class NzImagePreviewComponent implements OnInit {
}
}

private handlerImageTransformationWhileZoomingWithMouse(event: WheelEvent, deltaY: number): void {
let scaleValue: number;
const imageElement = this.imageRef.nativeElement;

const elementTransform = getComputedStyle(imageElement).transform;
const matrixValue = elementTransform.match(/matrix.*\((.+)\)/);

if (matrixValue) {
scaleValue = +matrixValue[1].split(', ')[0];
} else {
scaleValue = this.zoom;
}

const x = (event.clientX - imageElement.getBoundingClientRect().x) / scaleValue;
const y = (event.clientY - imageElement.getBoundingClientRect().y) / scaleValue;
const halfOfScaleStepValue = deltaY < 0 ? this.scaleStep / 2 : -this.scaleStep / 2;

this.position.x += -x * halfOfScaleStepValue * 2 + imageElement.offsetWidth * halfOfScaleStepValue;
this.position.y += -y * halfOfScaleStepValue * 2 + imageElement.offsetHeight * halfOfScaleStepValue;
}

private handleImageScaleWhileZoomingWithMouse(deltaY: number): void {
if (this.isZoomedInWithMouseWheel(deltaY)) {
this.onZoomIn();
} else {
this.onZoomOut();
}

if (this.zoom <= 1) {
this.reCenterImage();
}
}

private isZoomedInWithMouseWheel(delta: number): boolean {
return delta < 0;
}

private reset(): void {
this.zoom = this.config.nzZoom ?? this._defaultNzZoom;
this.scaleStep = this.config.nzScaleStep ?? this._defaultNzScaleStep;
this.rotate = this.config.nzRotate ?? this._defaultNzRotate;
this.flipHorizontally = false;
this.flipVertically = false;
this.reCenterImage();
}

private reCenterImage(): void {
this.position = { ...initialPosition };
}
}
107 changes: 102 additions & 5 deletions components/image/image.spec.ts
Expand Up @@ -5,7 +5,7 @@

import { LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes';
import { Overlay, OverlayContainer } from '@angular/cdk/overlay';
import { Component, DebugElement, NgModule, ViewChild } from '@angular/core';
import { Component, DebugElement, NgModule, NgZone, ViewChild } from '@angular/core';
import {
ComponentFixture,
discardPeriodicTasks,
Expand All @@ -30,8 +30,9 @@ import {
} from '@ant-design/icons-angular/icons';

import { NzConfigService } from 'ng-zorro-antd/core/config';
import { dispatchFakeEvent, dispatchKeyboardEvent } from 'ng-zorro-antd/core/testing';
import { NzIconModule, NZ_ICONS } from 'ng-zorro-antd/icon';
import { dispatchFakeEvent, dispatchKeyboardEvent, MockNgZone } from 'ng-zorro-antd/core/testing';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NZ_ICONS, NzIconModule } from 'ng-zorro-antd/icon';
import {
getFitContentPosition,
NzImage,
Expand All @@ -54,11 +55,21 @@ describe('Basics', () => {
let fixture: ComponentFixture<TestImageBasicsComponent>;
let context: TestImageBasicsComponent;
let debugElement: DebugElement;
let zone: MockNgZone;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [NzImageModule, TestImageModule, NoopAnimationsModule],
providers: [{ provide: Overlay, useClass: Overlay }]
providers: [
{ provide: Overlay, useClass: Overlay },
{
provide: NgZone,
useFactory: () => {
zone = new MockNgZone();
return zone;
}
}
]
});
TestBed.compileComponents();
}));
Expand Down Expand Up @@ -429,6 +440,40 @@ describe('Preview', () => {
flush();
}));

it('should detect mouse zoom direction correctly', fakeAsync(() => {
context.images = [{ src: QUICK_SRC }];
context.createUsingService();
const previewInstance = context.previewRef?.previewInstance!;
tickChanges();
previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
expect(previewInstance.isDragging).toEqual(true);
let isZoomingInside = previewInstance['isZoomedInWithMouseWheel'](10);
expect(isZoomingInside).toBeFalsy();
isZoomingInside = previewInstance['isZoomedInWithMouseWheel'](-10);
expect(isZoomingInside).toBeTruthy();
}));

it('should call correct methods when zooming in or out', fakeAsync(() => {
context.images = [{ src: QUICK_SRC }];
context.createUsingService();
const previewInstance = context.previewRef?.previewInstance!;
tickChanges();
previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
previewInstance['zoom'] = 5;
spyOn(previewInstance, 'onZoomOut');
spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
previewInstance['handleImageScaleWhileZoomingWithMouse'](10);
expect(previewInstance.onZoomOut).toHaveBeenCalled();
expect(previewInstance['reCenterImage']).not.toHaveBeenCalled();

previewInstance['zoom'] = 0.5;
spyOn(previewInstance, 'onZoomIn');
spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
previewInstance['handleImageScaleWhileZoomingWithMouse'](-10);
expect(previewInstance.onZoomOut).toHaveBeenCalled();
expect(previewInstance['reCenterImage']).toHaveBeenCalled();
}));

it('should container click work', fakeAsync(() => {
context.firstSrc = QUICK_SRC;
fixture.detectChanges();
Expand Down Expand Up @@ -556,10 +601,39 @@ describe('Preview', () => {
tickChanges();
previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
expect(previewInstance.isDragging).toEqual(true);
previewInstance.onDragReleased();
spyOn(previewInstance, 'onDragEnd').and.callFake(function () {
return true;
});
expect(previewInstance.position).toEqual({ x: 0, y: 0 });
}));

it('should onDragEnd be called after drag is ended', fakeAsync(() => {
context.images = [{ src: QUICK_SRC }];
context.createUsingService();
const previewInstance = context.previewRef?.previewInstance!;
tickChanges();
previewInstance.imagePreviewWrapper.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
spyOn(previewInstance, 'onDragEnd').and.callFake(function () {
return true;
});
const e: NzSafeAny = {};
previewInstance.onDragEnd(e);
expect(previewInstance['onDragEnd']).toHaveBeenCalled();
}));

it('should zoom to center when zoom is <= 1', fakeAsync(() => {
context.images = [{ src: QUICK_SRC }];
context.createUsingService();
const previewInstance = context.previewRef?.previewInstance!;
spyOn<NzSafeAny>(previewInstance, 'reCenterImage');
tickChanges();
context.zoomStep = 0.25;
(previewInstance as NzSafeAny).zoom = 1.1;
previewInstance.onZoomOut();
tickChanges();
expect(previewInstance['reCenterImage']).toHaveBeenCalled();
}));

it('should position calculate correct', () => {
let params = {
width: 200,
Expand Down Expand Up @@ -648,6 +722,29 @@ describe('Preview', () => {
expect(pos.y).toBe(-66);
});
});

describe('Zoom with mouse', () => {
it('should call proper methods', fakeAsync(() => {
context.images = [{ src: QUICK_SRC }];
context.createUsingService();
const previewInstance = context.previewRef?.previewInstance!;
tickChanges();
const e = jasmine.createSpyObj('e', ['preventDefault', 'stopPropagation']);
spyOn<NzSafeAny>(previewInstance, 'handlerImageTransformationWhileZoomingWithMouse');
spyOn<NzSafeAny>(previewInstance, 'handleImageScaleWhileZoomingWithMouse');
spyOn<NzSafeAny>(previewInstance, 'updatePreviewImageWrapperTransform');
spyOn<NzSafeAny>(previewInstance, 'updatePreviewImageTransform');
spyOn<NzSafeAny>(previewInstance, 'markForCheck');
previewInstance.wheelZoomEventHandler(e);
expect(e.preventDefault).toHaveBeenCalled();
expect(e.stopPropagation).toHaveBeenCalled();
expect(previewInstance['handlerImageTransformationWhileZoomingWithMouse']).toHaveBeenCalled();
expect(previewInstance['handleImageScaleWhileZoomingWithMouse']).toHaveBeenCalled();
expect(previewInstance['updatePreviewImageWrapperTransform']).toHaveBeenCalled();
expect(previewInstance['updatePreviewImageTransform']).toHaveBeenCalled();
expect(previewInstance['markForCheck']).toHaveBeenCalled();
}));
});
});

@Component({
Expand Down

0 comments on commit 4235c29

Please sign in to comment.