Skip to content

Commit

Permalink
perf: use shared mouse event listeners across all resizable instances
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Lewis committed Mar 23, 2017
1 parent 05f7f7e commit 2a4b102
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 67 deletions.
92 changes: 92 additions & 0 deletions src/pointerEventListeners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Renderer, NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';

interface PointerEventCoordinate {
clientX: number;
clientY: number;
event: MouseEvent | TouchEvent;
}

export class PointerEventListeners {

public pointerDown: Observable<PointerEventCoordinate>;

public pointerMove: Observable<PointerEventCoordinate>;

public pointerUp: Observable<PointerEventCoordinate>;

private static instance: PointerEventListeners; // tslint:disable-line

public static getInstance(renderer: Renderer, zone: NgZone): PointerEventListeners {
if (!PointerEventListeners.instance) {
PointerEventListeners.instance = new PointerEventListeners(renderer, zone);
}
return PointerEventListeners.instance;
}

constructor(renderer: Renderer, zone: NgZone) {

zone.runOutsideAngular(() => {

this.pointerDown = new Observable((observer: Observer<PointerEventCoordinate>) => {

const unsubscribeMouseDown: Function = renderer.listenGlobal('document', 'mousedown', (event: MouseEvent) => {
observer.next({clientX: event.clientX, clientY: event.clientY, event});
});

const unsubscribeTouchStart: Function = renderer.listenGlobal('document', 'touchstart', (event: TouchEvent) => {
observer.next({clientX: event.touches[0].clientX, clientY: event.touches[0].clientY, event});
});

return () => {
unsubscribeMouseDown();
unsubscribeTouchStart();
};

}).share();

this.pointerMove = new Observable((observer: Observer<PointerEventCoordinate>) => {

const unsubscribeMouseMove: Function = renderer.listenGlobal('document', 'mousemove', (event: MouseEvent) => {
observer.next({clientX: event.clientX, clientY: event.clientY, event});
});

const unsubscribeTouchMove: Function = renderer.listenGlobal('document', 'touchmove', (event: TouchEvent) => {
observer.next({clientX: event.targetTouches[0].clientX, clientY: event.targetTouches[0].clientY, event});
});

return () => {
unsubscribeMouseMove();
unsubscribeTouchMove();
};

}).share();

this.pointerUp = new Observable((observer: Observer<PointerEventCoordinate>) => {

const unsubscribeMouseUp: Function = renderer.listenGlobal('document', 'mouseup', (event: MouseEvent) => {
observer.next({clientX: event.clientX, clientY: event.clientY, event});
});

const unsubscribeTouchEnd: Function = renderer.listenGlobal('document', 'touchend', (event: TouchEvent) => {
observer.next({clientX: event.changedTouches[0].clientX, clientY: event.changedTouches[0].clientY, event});
});

const unsubscribeTouchCancel: Function = renderer.listenGlobal('document', 'touchcancel', (event: TouchEvent) => {
observer.next({clientX: event.changedTouches[0].clientX, clientY: event.changedTouches[0].clientY, event});
});

return () => {
unsubscribeMouseUp();
unsubscribeTouchEnd();
unsubscribeTouchCancel();
};

}).share();

});

}

}
126 changes: 59 additions & 67 deletions src/resizable.directive.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Directive,
HostListener,
Renderer,
ElementRef,
OnInit,
Expand Down Expand Up @@ -28,6 +27,7 @@ import 'rxjs/add/operator/share';
import {ResizeHandle} from './resizeHandle.directive';
import {Edges} from './interfaces/edges.interface';
import {BoundingRectangle} from './interfaces/boundingRectangle.interface';
import {PointerEventListeners} from './pointerEventListeners';

interface Coordinate {
x: number;
Expand All @@ -39,7 +39,7 @@ function isNumberCloseTo(value1: number, value2: number, precision: number = 3):
return diff < precision;
}

function getNewBoundingRectangle(startingRect: BoundingRectangle, edges: Edges, mouseX: number, mouseY: number): BoundingRectangle {
function getNewBoundingRectangle(startingRect: BoundingRectangle, edges: Edges, clientX: number, clientY: number): BoundingRectangle {

const newBoundingRect: BoundingRectangle = {
top: startingRect.top,
Expand All @@ -49,16 +49,16 @@ function getNewBoundingRectangle(startingRect: BoundingRectangle, edges: Edges,
};

if (edges.top) {
newBoundingRect.top += mouseY;
newBoundingRect.top += clientY;
}
if (edges.bottom) {
newBoundingRect.bottom += mouseY;
newBoundingRect.bottom += clientY;
}
if (edges.left) {
newBoundingRect.left += mouseX;
newBoundingRect.left += clientX;
}
if (edges.right) {
newBoundingRect.right += mouseX;
newBoundingRect.right += clientX;
}
newBoundingRect.height = newBoundingRect.bottom - newBoundingRect.top;
newBoundingRect.width = newBoundingRect.right - newBoundingRect.left;
Expand Down Expand Up @@ -86,48 +86,48 @@ function getElementRect(element: ElementRef, ghostElementPositioning: string): B
}
}

function isWithinBoundingY({mouseY, rect}: {mouseY: number, rect: ClientRect}): boolean {
return mouseY >= rect.top && mouseY <= rect.bottom;
function isWithinBoundingY({clientY, rect}: {clientY: number, rect: ClientRect}): boolean {
return clientY >= rect.top && clientY <= rect.bottom;
}

function isWithinBoundingX({mouseX, rect}: {mouseX: number, rect: ClientRect}): boolean {
return mouseX >= rect.left && mouseX <= rect.right;
function isWithinBoundingX({clientX, rect}: {clientX: number, rect: ClientRect}): boolean {
return clientX >= rect.left && clientX <= rect.right;
}

function getResizeEdges(
{mouseX, mouseY, elm, allowedEdges, cursorPrecision}:
{mouseX: number, mouseY: number, elm: ElementRef, allowedEdges: Edges, cursorPrecision: number}): Edges {
{clientX, clientY, elm, allowedEdges, cursorPrecision}:
{clientX: number, clientY: number, elm: ElementRef, allowedEdges: Edges, cursorPrecision: number}): Edges {
const elmPosition: ClientRect = elm.nativeElement.getBoundingClientRect();
const edges: Edges = {};

if (
allowedEdges.left &&
isNumberCloseTo(mouseX, elmPosition.left, cursorPrecision) &&
isWithinBoundingY({mouseY, rect: elmPosition})
isNumberCloseTo(clientX, elmPosition.left, cursorPrecision) &&
isWithinBoundingY({clientY, rect: elmPosition})
) {
edges.left = true;
}

if (
allowedEdges.right &&
isNumberCloseTo(mouseX, elmPosition.right, cursorPrecision) &&
isWithinBoundingY({mouseY, rect: elmPosition})
isNumberCloseTo(clientX, elmPosition.right, cursorPrecision) &&
isWithinBoundingY({clientY, rect: elmPosition})
) {
edges.right = true;
}

if (
allowedEdges.top &&
isNumberCloseTo(mouseY, elmPosition.top, cursorPrecision) &&
isWithinBoundingX({mouseX, rect: elmPosition})
isNumberCloseTo(clientY, elmPosition.top, cursorPrecision) &&
isWithinBoundingX({clientX, rect: elmPosition})
) {
edges.top = true;
}

if (
allowedEdges.bottom &&
isNumberCloseTo(mouseY, elmPosition.bottom, cursorPrecision) &&
isWithinBoundingX({mouseX, rect: elmPosition})
isNumberCloseTo(clientY, elmPosition.bottom, cursorPrecision) &&
isWithinBoundingX({clientX, rect: elmPosition})
) {
edges.bottom = true;
}
Expand Down Expand Up @@ -276,16 +276,39 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChildren(ResizeHandle) resizeHandles: QueryList<ResizeHandle>;

private pointerEventListeners: PointerEventListeners;

private pointerEventListenerSubscriptions: any = {};

/**
* @private
*/
constructor(private renderer: Renderer, public elm: ElementRef, private zone: NgZone) {}
constructor(
private renderer: Renderer,
public elm: ElementRef,
private zone: NgZone
) {
this.pointerEventListeners = PointerEventListeners.getInstance(renderer, zone);
}

/**
* @private
*/
ngOnInit(): void {

// TODO - use some fancy Observable.merge's for this
this.pointerEventListenerSubscriptions.pointerDown = this.pointerEventListeners.pointerDown.subscribe(({clientX, clientY}) => {
this.mousedown.next({clientX, clientY});
});

this.pointerEventListenerSubscriptions.pointerMove = this.pointerEventListeners.pointerMove.subscribe(({clientX, clientY, event}) => {
this.mousemove.next({clientX, clientY, event});
});

this.pointerEventListenerSubscriptions.pointerUp = this.pointerEventListeners.pointerUp.subscribe(({clientX, clientY}) => {
this.mouseup.next({clientX, clientY});
});

let currentResize: {
edges: Edges,
startingRect: BoundingRectangle,
Expand All @@ -308,10 +331,10 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {
event.preventDefault();
});

mouseMove.throttle(val => interval(MOUSE_MOVE_THROTTLE_MS)).subscribe(({mouseX, mouseY}) => {
mouseMove.throttle(val => interval(MOUSE_MOVE_THROTTLE_MS)).subscribe(({clientX, clientY}) => {

const resizeEdges: Edges = getResizeEdges({
mouseX, mouseY,
clientX, clientY,
elm: this.elm,
allowedEdges: this.resizeEdges,
cursorPrecision: this.resizeCursorPrecision
Expand All @@ -332,8 +355,8 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {

const getDiff: Function = moveCoords => {
return {
mouseX: moveCoords.mouseX - startCoords.mouseX,
mouseY: moveCoords.mouseY - startCoords.mouseY
clientX: moveCoords.clientX - startCoords.clientX,
clientY: moveCoords.clientY - startCoords.clientY
};
};

Expand All @@ -359,8 +382,8 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {

const getGrid: Function = (coords, snapGrid) => {
return {
x: Math.ceil(coords.mouseX / snapGrid.x),
y: Math.ceil(coords.mouseY / snapGrid.y)
x: Math.ceil(coords.clientX / snapGrid.x),
y: Math.ceil(coords.clientY / snapGrid.y)
};
};

Expand All @@ -384,15 +407,15 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {
}).map(([, newCoords]) => {
const snapGrid: Coordinate = getSnapGrid();
return {
mouseX: Math.round(newCoords.mouseX / snapGrid.x) * snapGrid.x,
mouseY: Math.round(newCoords.mouseY / snapGrid.y) * snapGrid.y
clientX: Math.round(newCoords.clientX / snapGrid.x) * snapGrid.x,
clientY: Math.round(newCoords.clientY / snapGrid.y) * snapGrid.y
};
}).takeUntil(merge(this.mouseup, this.mousedown));

}).filter(() => !!currentResize);

mousedrag.map(({mouseX, mouseY}) => {
return getNewBoundingRectangle(currentResize.startingRect, currentResize.edges, mouseX, mouseY);
mousedrag.map(({clientX, clientY}) => {
return getNewBoundingRectangle(currentResize.startingRect, currentResize.edges, clientX, clientY);
}).filter((newBoundingRect: BoundingRectangle) => {
return newBoundingRect.height > 0 && newBoundingRect.width > 0;
}).filter((newBoundingRect: BoundingRectangle) => {
Expand Down Expand Up @@ -428,9 +451,9 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {

});

this.mousedown.map(({mouseX, mouseY, edges}) => {
this.mousedown.map(({clientX, clientY, edges}) => {
return edges || getResizeEdges({
mouseX, mouseY,
clientX, clientY,
elm: this.elm,
allowedEdges: this.resizeEdges,
cursorPrecision: this.resizeCursorPrecision
Expand Down Expand Up @@ -503,40 +526,9 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit {
this.mousedown.complete();
this.mouseup.complete();
this.mousemove.complete();
}

/**
* @private
*/
@HostListener('document:touchstart', ['$event.touches[0].clientX', '$event.touches[0].clientY'])
@HostListener('document:mousedown', ['$event.clientX', '$event.clientY'])
onMousedown(mouseX: number, mouseY: number): void {
this.zone.runOutsideAngular(() => {
this.mousedown.next({mouseX, mouseY});
});
}

/**
* @private
*/
@HostListener('document:touchmove', ['$event', '$event.targetTouches[0].clientX', '$event.targetTouches[0].clientY'])
@HostListener('document:mousemove', ['$event', '$event.clientX', '$event.clientY'])
onMousemove(event: any, mouseX: number, mouseY: number): void {
this.zone.runOutsideAngular(() => {
this.mousemove.next({mouseX, mouseY, event});
});
}

/**
* @private
*/
@HostListener('document:touchend', ['$event.changedTouches[0].clientX', '$event.changedTouches[0].clientY'])
@HostListener('document:touchcancel', ['$event.changedTouches[0].clientX', '$event.changedTouches[0].clientY'])
@HostListener('document:mouseup', ['$event.clientX', '$event.clientY'])
onMouseup(mouseX: number, mouseY: number): void {
this.zone.runOutsideAngular(() => {
this.mouseup.next({mouseX, mouseY});
});
this.pointerEventListenerSubscriptions.pointerDown.unsubscribe();
this.pointerEventListenerSubscriptions.pointerMove.unsubscribe();
this.pointerEventListenerSubscriptions.pointerUp.unsubscribe();
}

}

0 comments on commit 2a4b102

Please sign in to comment.