From 66afcf0bf7fe4720a5d206158002e8e844cd4d59 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Mon, 9 Sep 2019 09:48:40 +0800 Subject: [PATCH] perf(module:resizable): listen document events when resizing start (#4021) --- components/core/testing/event-objects.ts | 3 +- components/resizable/nz-resizable-utils.ts | 12 +++ .../resizable/nz-resizable.directive.ts | 82 ++++++++++--------- components/resizable/nz-resizable.service.ts | 47 ++++++++++- components/resizable/nz-resizable.spec.ts | 70 +++++++++++----- .../resizable/nz-resize-handle.component.ts | 9 +- components/resizable/public-api.ts | 1 + 7 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 components/resizable/nz-resizable-utils.ts diff --git a/components/core/testing/event-objects.ts b/components/core/testing/event-objects.ts index eeb7e881e9a..8cf25187084 100644 --- a/components/core/testing/event-objects.ts +++ b/components/core/testing/event-objects.ts @@ -40,8 +40,7 @@ export function createTouchEvent(type: string, pageX: number = 0, pageY: number // In favor of creating events that work for most of the browsers, the event is created // as a basic UI Event. The necessary details for the event will be set manually. const event = document.createEvent('UIEvent'); - const touchDetails = { pageX, pageY }; - + const touchDetails = { pageX, pageY, clientX: pageX, clientY: pageY }; event.initUIEvent(type, true, true, window, 0); // Most of the browsers don't have a "initTouchEvent" method that can be used to define diff --git a/components/resizable/nz-resizable-utils.ts b/components/resizable/nz-resizable-utils.ts new file mode 100644 index 00000000000..c279e89e788 --- /dev/null +++ b/components/resizable/nz-resizable-utils.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ +import { isTouchEvent } from 'ng-zorro-antd/core'; + +export function getEventWithPoint(event: MouseEvent | TouchEvent): MouseEvent | Touch { + return isTouchEvent(event) ? event.touches[0] || event.changedTouches[0] : (event as MouseEvent); +} diff --git a/components/resizable/nz-resizable.directive.ts b/components/resizable/nz-resizable.directive.ts index 8eb48497158..f2c2b0d970e 100644 --- a/components/resizable/nz-resizable.directive.ts +++ b/components/resizable/nz-resizable.directive.ts @@ -12,7 +12,6 @@ import { Directive, ElementRef, EventEmitter, - HostListener, Input, NgZone, OnDestroy, @@ -24,6 +23,7 @@ import { ensureInBounds, InputBoolean } from 'ng-zorro-antd/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { getEventWithPoint } from './nz-resizable-utils'; import { NzResizableService } from './nz-resizable.service'; import { NzResizeHandleMouseDownEvent } from './nz-resize-handle.component'; @@ -31,7 +31,7 @@ export interface NzResizeEvent { width?: number; height?: number; col?: number; - mouseEvent?: MouseEvent; + mouseEvent?: MouseEvent | TouchEvent; } @Directive({ @@ -62,7 +62,7 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { resizing = false; private elRect: ClientRect | DOMRect; - private currentHandleEvent: NzResizeHandleMouseDownEvent; + private currentHandleEvent: NzResizeHandleMouseDownEvent | null; private ghostElement: HTMLDivElement | null; private el: HTMLElement; private sizeCache: NzResizeEvent | null; @@ -71,12 +71,13 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { constructor( private elementRef: ElementRef, private renderer: Renderer2, - private ngZone: NgZone, private nzResizableService: NzResizableService, - private platform: Platform + private platform: Platform, + private ngZone: NgZone ) { this.nzResizableService.handleMouseDown$.pipe(takeUntil(this.destroy$)).subscribe(event => { this.resizing = true; + this.nzResizableService.startResizing(event.mouseEvent); this.currentHandleEvent = event; this.setCursor(); this.nzResizeStart.emit({ @@ -84,24 +85,18 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { }); this.elRect = this.el.getBoundingClientRect(); }); - } - @HostListener('document:mouseup', ['$event']) - onMouseup($event: MouseEvent): void { - this.ngZone.runOutsideAngular(() => { + this.nzResizableService.documentMouseUp$.pipe(takeUntil(this.destroy$)).subscribe(event => { if (this.resizing) { this.resizing = false; this.nzResizableService.documentMouseUp$.next(); - this.endResize($event); + this.endResize(event); } }); - } - @HostListener('document:mousemove', ['$event']) - onMousemove($event: MouseEvent): void { - this.ngZone.runOutsideAngular(() => { + this.nzResizableService.documentMouseMove$.pipe(takeUntil(this.destroy$)).subscribe(event => { if (this.resizing) { - this.resize($event); + this.resize(event); } }); } @@ -124,11 +119,11 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { calcSize(width: number, height: number, ratio: number): NzResizeEvent { let newWidth: number; let newHeight: number; + let maxWidth: number; + let maxHeight: number; let col = 0; let spanWidth = 0; - let maxWidth = Infinity; let minWidth = this.nzMinWidth; - let maxHeight = Infinity; let boundWidth = Infinity; let boundHeight = Infinity; if (this.nzBounds === 'parent') { @@ -159,7 +154,7 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { } if (ratio !== -1) { - if (/(left|right)/i.test(this.currentHandleEvent.direction)) { + if (/(left|right)/i.test(this.currentHandleEvent!.direction)) { newWidth = Math.min(Math.max(width, minWidth), maxWidth); newHeight = Math.min(Math.max(newWidth / ratio, this.nzMinHeight), maxHeight); if (newHeight >= maxHeight || newHeight <= this.nzMinHeight) { @@ -190,7 +185,7 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { } setCursor(): void { - switch (this.currentHandleEvent.direction) { + switch (this.currentHandleEvent!.direction) { case 'left': case 'right': this.renderer.setStyle(document.body, 'cursor', 'col-resize'); @@ -211,52 +206,56 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { this.renderer.setStyle(document.body, 'user-select', 'none'); } - resize($event: MouseEvent): void { + resize(event: MouseEvent | TouchEvent): void { const elRect = this.elRect; + const resizeEvent = getEventWithPoint(event); + const handleEvent = getEventWithPoint(this.currentHandleEvent!.mouseEvent); let width = elRect.width; let height = elRect.height; const ratio = this.nzLockAspectRatio ? width / height : -1; - switch (this.currentHandleEvent.direction) { + switch (this.currentHandleEvent!.direction) { case 'bottomRight': - width = $event.clientX - elRect.left; - height = $event.clientY - elRect.top; + width = resizeEvent.clientX - elRect.left; + height = resizeEvent.clientY - elRect.top; break; case 'bottomLeft': - width = elRect.width + this.currentHandleEvent.mouseEvent.clientX - $event.clientX; - height = $event.clientY - elRect.top; + width = elRect.width + handleEvent.clientX - resizeEvent.clientX; + height = resizeEvent.clientY - elRect.top; break; case 'topRight': - width = $event.clientX - elRect.left; - height = elRect.height + this.currentHandleEvent.mouseEvent.clientY - $event.clientY; + width = resizeEvent.clientX - elRect.left; + height = elRect.height + handleEvent.clientY - resizeEvent.clientY; break; case 'topLeft': - width = elRect.width + this.currentHandleEvent.mouseEvent.clientX - $event.clientX; - height = elRect.height + this.currentHandleEvent.mouseEvent.clientY - $event.clientY; + width = elRect.width + handleEvent.clientX - resizeEvent.clientX; + height = elRect.height + handleEvent.clientY - resizeEvent.clientY; break; case 'top': - height = elRect.height + this.currentHandleEvent.mouseEvent.clientY - $event.clientY; + height = elRect.height + handleEvent.clientY - resizeEvent.clientY; break; case 'right': - width = $event.clientX - elRect.left; + width = resizeEvent.clientX - elRect.left; break; case 'bottom': - height = $event.clientY - elRect.top; + height = resizeEvent.clientY - elRect.top; break; case 'left': - width = elRect.width + this.currentHandleEvent.mouseEvent.clientX - $event.clientX; + width = elRect.width + handleEvent.clientX - resizeEvent.clientX; } const size = this.calcSize(width, height, ratio); this.sizeCache = { ...size }; - this.nzResize.emit({ - ...size, - mouseEvent: $event + this.ngZone.run(() => { + this.nzResize.emit({ + ...size, + mouseEvent: event + }); }); if (this.nzPreview) { this.previewResize(size); } } - endResize($event: MouseEvent): void { + endResize(event: MouseEvent | TouchEvent): void { this.renderer.setStyle(document.body, 'cursor', ''); this.renderer.setStyle(document.body, 'user-select', ''); this.removeGhostElement(); @@ -266,11 +265,14 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy { width: this.elRect.width, height: this.elRect.height }; - this.nzResizeEnd.emit({ - ...size, - mouseEvent: $event + this.ngZone.run(() => { + this.nzResizeEnd.emit({ + ...size, + mouseEvent: event + }); }); this.sizeCache = null; + this.currentHandleEvent = null; } previewResize({ width, height }: NzResizeEvent): void { diff --git a/components/resizable/nz-resizable.service.ts b/components/resizable/nz-resizable.service.ts index e446498b4e0..8d63e3880bf 100644 --- a/components/resizable/nz-resizable.service.ts +++ b/components/resizable/nz-resizable.service.ts @@ -6,23 +6,64 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -import { Injectable, OnDestroy } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs'; +import { isTouchEvent } from 'ng-zorro-antd/core'; import { NzResizeHandleMouseDownEvent } from './nz-resize-handle.component'; @Injectable() export class NzResizableService implements OnDestroy { + private document: Document; + private listeners = new Map void>(); + handleMouseDown$ = new Subject(); - documentMouseUp$ = new Subject(); + documentMouseUp$ = new Subject(); + documentMouseMove$ = new Subject(); mouseEntered$ = new Subject(); - constructor() {} + // tslint:disable-next-line:no-any + constructor(private ngZone: NgZone, @Inject(DOCUMENT) document: any) { + this.document = document; + } + + startResizing(event: MouseEvent | TouchEvent): void { + const _isTouchEvent = isTouchEvent(event); + this.clearListeners(); + const moveEvent = _isTouchEvent ? 'touchmove' : 'mousemove'; + const upEvent = _isTouchEvent ? 'touchend' : 'mouseup'; + const moveEventHandler = (e: MouseEvent | TouchEvent) => { + this.documentMouseMove$.next(e); + }; + const upEventHandler = (e: MouseEvent | TouchEvent) => { + this.documentMouseUp$.next(e); + this.clearListeners(); + }; + + this.listeners.set(moveEvent, moveEventHandler); + this.listeners.set(upEvent, upEventHandler); + + this.ngZone.runOutsideAngular(() => { + this.listeners.forEach((handler, name) => { + this.document.addEventListener(name, handler as EventListener); + }); + }); + } + + private clearListeners(): void { + this.listeners.forEach((handler, name) => { + this.document.removeEventListener(name, handler as EventListener); + }); + this.listeners.clear(); + } ngOnDestroy(): void { this.handleMouseDown$.complete(); this.documentMouseUp$.complete(); + this.documentMouseMove$.complete(); this.mouseEntered$.complete(); + this.clearListeners(); } } diff --git a/components/resizable/nz-resizable.spec.ts b/components/resizable/nz-resizable.spec.ts index 005cef87def..96a28a42780 100644 --- a/components/resizable/nz-resizable.spec.ts +++ b/components/resizable/nz-resizable.spec.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, NgZone, ViewChild } from '@angular/core'; import { fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { dispatchMouseEvent, MockNgZone } from 'ng-zorro-antd/core'; +import { dispatchMouseEvent, dispatchTouchEvent, MockNgZone } from 'ng-zorro-antd/core'; import { NzGridModule } from 'ng-zorro-antd/grid'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; @@ -80,7 +80,7 @@ describe('resizable', () => { it('should maximum size work', fakeAsync(() => { const rect = resizableEle.getBoundingClientRect(); const handel = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handel, { x: rect.right, @@ -103,7 +103,7 @@ describe('resizable', () => { it('should minimum size work', fakeAsync(() => { const rect = resizableEle.getBoundingClientRect(); const handel = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handel, { x: rect.right, @@ -133,6 +133,26 @@ describe('resizable', () => { expect(testComponent.height).toBe(200); }); + it('should touch event work', fakeAsync(() => { + const handle = resizableEle.querySelector('.nz-resizable-handle-top') as HTMLElement; + touchMoveTrigger( + handle, + { + x: rect.left, + y: rect.top + }, + { + x: rect.left, + y: rect.top + 100 + } + ); + fixture.detectChanges(); + tick(16); + fixture.detectChanges(); + expect(testComponent.height).toBeLessThanOrEqual(200); + expect(testComponent.height).toBeGreaterThanOrEqual(100); + })); + /** * +---↓---+ * | | @@ -140,7 +160,7 @@ describe('resizable', () => { */ it('top', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-top') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.left, @@ -165,7 +185,7 @@ describe('resizable', () => { */ it('bottom', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-bottom') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.left, @@ -190,7 +210,7 @@ describe('resizable', () => { */ it('left', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-left') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.left, @@ -215,7 +235,7 @@ describe('resizable', () => { */ it('right', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-right') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -240,7 +260,7 @@ describe('resizable', () => { */ it('topRight', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-topRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -267,7 +287,7 @@ describe('resizable', () => { */ it('topLeft', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-topLeft') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.left, @@ -294,7 +314,7 @@ describe('resizable', () => { */ it('bottomRight', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -321,7 +341,7 @@ describe('resizable', () => { */ it('bottomLeft', fakeAsync(() => { const handle = resizableEle.querySelector('.nz-resizable-handle-bottomLeft') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.left, @@ -362,7 +382,7 @@ describe('resizable', () => { expect(rightHandel.querySelector('.right-wrap')).toBeTruthy(); const rect = resizableEle.getBoundingClientRect(); - moveTrigger( + mouseMoveTrigger( bottomRightHandel, { x: rect.right, @@ -402,7 +422,7 @@ describe('resizable', () => { const topHandel = resizableEle.querySelector('.nz-resizable-handle-top') as HTMLElement; const bottomRightHandel = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; const ratio = testComponent.width / testComponent.height; - moveTrigger( + mouseMoveTrigger( leftHandel, { x: rect.right, @@ -416,7 +436,7 @@ describe('resizable', () => { fixture.detectChanges(); tick(16); fixture.detectChanges(); - moveTrigger( + mouseMoveTrigger( bottomRightHandel, { x: rect.right, @@ -430,7 +450,7 @@ describe('resizable', () => { fixture.detectChanges(); tick(16); fixture.detectChanges(); - moveTrigger( + mouseMoveTrigger( topHandel, { x: rect.right, @@ -488,7 +508,7 @@ describe('resizable', () => { it('should grid work', fakeAsync(() => { const rect = resizableEle.getBoundingClientRect(); const handle = resizableEle.querySelector('.nz-resizable-handle-right') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -503,7 +523,7 @@ describe('resizable', () => { tick(16); fixture.detectChanges(); expect(testComponent.col).toBe(3); - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -536,7 +556,7 @@ describe('resizable', () => { it('should parent bounds work', fakeAsync(() => { const rect = resizableEle.getBoundingClientRect(); const handle = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -559,7 +579,7 @@ describe('resizable', () => { testComponent.bounds = testComponent.boxRef; fixture.detectChanges(); const handle = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -582,7 +602,7 @@ describe('resizable', () => { testComponent.bounds = 'window'; fixture.detectChanges(); const handle = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement; - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -601,7 +621,7 @@ describe('resizable', () => { testComponent.maxHeight = window.innerHeight * 2; testComponent.maxWidth = window.innerWidth * 2; fixture.detectChanges(); - moveTrigger( + mouseMoveTrigger( handle, { x: rect.right, @@ -621,12 +641,18 @@ describe('resizable', () => { }); }); -function moveTrigger(el: HTMLElement, from: { x: number; y: number }, to: { x: number; y: number }): void { +function mouseMoveTrigger(el: HTMLElement, from: { x: number; y: number }, to: { x: number; y: number }): void { dispatchMouseEvent(el, 'mousedown', from.x, from.y); dispatchMouseEvent(window.document, 'mousemove', to.x, to.y); dispatchMouseEvent(window.document, 'mouseup'); } +function touchMoveTrigger(el: HTMLElement, from: { x: number; y: number }, to: { x: number; y: number }): void { + dispatchTouchEvent(el, 'touchstart', from.x, from.y); + dispatchTouchEvent(window.document, 'touchmove', to.x, to.y); + dispatchTouchEvent(window.document, 'touchend'); +} + @Component({ template: `
diff --git a/components/resizable/nz-resize-handle.component.ts b/components/resizable/nz-resize-handle.component.ts index f54ea801c8a..504f5db3634 100644 --- a/components/resizable/nz-resize-handle.component.ts +++ b/components/resizable/nz-resize-handle.component.ts @@ -32,7 +32,7 @@ export type NzResizeDirection = | 'topLeft'; export class NzResizeHandleMouseDownEvent { - constructor(public direction: NzResizeDirection, public mouseEvent: MouseEvent) {} + constructor(public direction: NzResizeDirection, public mouseEvent: MouseEvent | TouchEvent) {} } @Component({ @@ -43,7 +43,8 @@ export class NzResizeHandleMouseDownEvent { host: { '[class]': '"nz-resizable-handle nz-resizable-handle-" + nzDirection', '[class.nz-resizable-handle-box-hover]': 'entered', - '(mousedown)': 'onMousedown($event)' + '(mousedown)': 'onMousedown($event)', + '(touchstart)': 'onMousedown($event)' } }) export class NzResizeHandleComponent implements OnInit, OnDestroy { @@ -62,8 +63,8 @@ export class NzResizeHandleComponent implements OnInit, OnDestroy { }); } - onMousedown($event: MouseEvent): void { - this.nzResizableService.handleMouseDown$.next(new NzResizeHandleMouseDownEvent(this.nzDirection, $event)); + onMousedown(event: MouseEvent | TouchEvent): void { + this.nzResizableService.handleMouseDown$.next(new NzResizeHandleMouseDownEvent(this.nzDirection, event)); } ngOnDestroy(): void { diff --git a/components/resizable/public-api.ts b/components/resizable/public-api.ts index a2d1377d64b..398c7c75199 100644 --- a/components/resizable/public-api.ts +++ b/components/resizable/public-api.ts @@ -11,3 +11,4 @@ export * from './nz-resizable.directive'; export * from './nz-resizable.service'; export * from './nz-resize-handles.component'; export * from './nz-resize-handle.component'; +export * from './nz-resizable-utils';