From 1af705aa7b152bf31506a4d9e04614a93e3a8a2d Mon Sep 17 00:00:00 2001 From: Matt Lewis Date: Sun, 26 Jun 2016 14:53:00 +0100 Subject: [PATCH] feat(resizeHandles): add support for nesting resize handles inside the element Closes #10 --- demo/demo.ts | 16 ++++- src/resizable.directive.ts | 107 +++++++++++++++++++---------- test/resizable.spec.ts | 134 ++++++++++++++++++++++++++++++------- 3 files changed, 194 insertions(+), 63 deletions(-) diff --git a/demo/demo.ts b/demo/demo.ts index 0e2d26e..ae4d291 100644 --- a/demo/demo.ts +++ b/demo/demo.ts @@ -1,10 +1,10 @@ import {Component} from '@angular/core'; import {NgStyle} from '@angular/common'; -import {Resizable, ResizeEvent} from './../angular2-resizable'; +import {Resizable, ResizeEvent, ResizeHandle} from './../angular2-resizable'; @Component({ selector: 'demo-app', - directives: [Resizable, NgStyle], + directives: [Resizable, ResizeHandle, NgStyle], styles: [` .rectangle { position: relative; @@ -19,6 +19,12 @@ import {Resizable, ResizeEvent} from './../angular2-resizable'; color: #121621; margin: auto; } + .resize-handle { + position: absolute; + bottom: 10px; + right: 10px; + -webkit-user-drag: none; + } `], template: `
@@ -27,9 +33,13 @@ import {Resizable, ResizeEvent} from './../angular2-resizable'; class="rectangle" [ngStyle]="style" mwl-resizable - [resizeEdges]="{left: true, right: true, top: true, bottom: true}" [validateResize]="validate" (onResizeEnd)="onResizeEnd($event)"> +
` diff --git a/src/resizable.directive.ts b/src/resizable.directive.ts index c0072ca..8d37a83 100644 --- a/src/resizable.directive.ts +++ b/src/resizable.directive.ts @@ -4,9 +4,12 @@ import { Renderer, ElementRef, OnInit, + AfterViewInit, Output, Input, - EventEmitter + EventEmitter, + ContentChildren, + QueryList } from '@angular/core'; import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; @@ -106,20 +109,47 @@ const getResizeCursor: Function = (edges: Edges): string => { } }; +@Directive({ + selector: '[mwl-resize-handle]' +}) +export class ResizeHandle { + + @Input() resizeEdges: Edges = {}; + + public resizable: Resizable; // set by the parent mwl-resizable directive + + @HostListener('mouseup', ['$event.clientX', '$event.clientY']) + private onMouseup(mouseX: number, mouseY: number): void { + this.resizable.mouseup.next({mouseX, mouseY, edges: this.resizeEdges}); + } + + @HostListener('mousedown', ['$event.clientX', '$event.clientY']) + private onMousedown(mouseX: number, mouseY: number): void { + this.resizable.mousedown.next({mouseX, mouseY, edges: this.resizeEdges}); + } + + @HostListener('mousemove', ['$event.clientX', '$event.clientY']) + private onMousemove(mouseX: number, mouseY: number): void { + this.resizable.mousemove.next({mouseX, mouseY, edges: this.resizeEdges}); + } + +} + @Directive({ selector: '[mwl-resizable]' }) -export class Resizable implements OnInit { +export class Resizable implements OnInit, AfterViewInit { @Input() validateResize: Function; @Input() resizeEdges: Edges = {}; @Output() onResizeStart: EventEmitter = new EventEmitter(false); @Output() onResize: EventEmitter = new EventEmitter(false); @Output() onResizeEnd: EventEmitter = new EventEmitter(false); + @ContentChildren(ResizeHandle) resizeHandles: QueryList; - private mouseup: Subject = new Subject(); - private mousedown: Subject = new Subject(); - private mousemove: Subject = new Subject(); + public mouseup: Subject = new Subject(); + public mousedown: Subject = new Subject(); + public mousemove: Subject = new Subject(); constructor(private renderer: Renderer, private elm: ElementRef) {} @@ -191,37 +221,38 @@ export class Resizable implements OnInit { }); - this.mousedown.subscribe(({mouseX, mouseY}) => { - const edges: Edges = getResizeEdges({mouseX, mouseY, elm: this.elm, allowedEdges: this.resizeEdges}); - if (Object.keys(edges).length > 0) { - if (currentResize) { - resetElementStyles(); - } - const startingRect: BoundingRectangle = this.elm.nativeElement.getBoundingClientRect(); - currentResize = { - edges, - startingRect, - currentRect: startingRect, - originalStyles: { - position: this.elm.nativeElement.style.position, - left: this.elm.nativeElement.style.left, - top: this.elm.nativeElement.style.top, - width: `${startingRect.width}px`, - height: `${startingRect.height}px`, - 'user-drag': this.elm.nativeElement.style['user-drag'], - '-webkit-user-drag': this.elm.nativeElement.style['-webkit-user-drag'] - } - }; - this.renderer.setElementStyle(this.elm.nativeElement, 'position', 'fixed'); - this.renderer.setElementStyle(this.elm.nativeElement, 'left', `${currentResize.startingRect.left}px`); - this.renderer.setElementStyle(this.elm.nativeElement, 'top', `${currentResize.startingRect.top}px`); - this.renderer.setElementStyle(this.elm.nativeElement, 'user-drag', 'none'); - this.renderer.setElementStyle(this.elm.nativeElement, '-webkit-user-drag', 'none'); - this.onResizeStart.emit({ - edges, - rectangle: getNewBoundingRectangle(startingRect, {}, 0, 0) - }); + this.mousedown.map(({mouseX, mouseY, edges}) => { + return edges || getResizeEdges({mouseX, mouseY, elm: this.elm, allowedEdges: this.resizeEdges}); + }).filter((edges: Edges) => { + return Object.keys(edges).length > 0; + }).subscribe((edges: Edges) => { + if (currentResize) { + resetElementStyles(); } + const startingRect: BoundingRectangle = this.elm.nativeElement.getBoundingClientRect(); + currentResize = { + edges, + startingRect, + currentRect: startingRect, + originalStyles: { + position: this.elm.nativeElement.style.position, + left: this.elm.nativeElement.style.left, + top: this.elm.nativeElement.style.top, + width: `${startingRect.width}px`, + height: `${startingRect.height}px`, + 'user-drag': this.elm.nativeElement.style['user-drag'], + '-webkit-user-drag': this.elm.nativeElement.style['-webkit-user-drag'] + } + }; + this.renderer.setElementStyle(this.elm.nativeElement, 'position', 'fixed'); + this.renderer.setElementStyle(this.elm.nativeElement, 'left', `${currentResize.startingRect.left}px`); + this.renderer.setElementStyle(this.elm.nativeElement, 'top', `${currentResize.startingRect.top}px`); + this.renderer.setElementStyle(this.elm.nativeElement, 'user-drag', 'none'); + this.renderer.setElementStyle(this.elm.nativeElement, '-webkit-user-drag', 'none'); + this.onResizeStart.emit({ + edges, + rectangle: getNewBoundingRectangle(startingRect, {}, 0, 0) + }); }); this.mouseup.subscribe(() => { @@ -237,6 +268,12 @@ export class Resizable implements OnInit { } + ngAfterViewInit(): void { + this.resizeHandles.forEach((handle: ResizeHandle) => { + handle.resizable = this; + }); + } + @HostListener('document:mouseup', ['$event.clientX', '$event.clientY']) private onMouseup(mouseX: number, mouseY: number): void { this.mouseup.next({mouseX, mouseY}); diff --git a/test/resizable.spec.ts b/test/resizable.spec.ts index 5778657..d66a047 100644 --- a/test/resizable.spec.ts +++ b/test/resizable.spec.ts @@ -1,6 +1,6 @@ import {Component, ViewChild} from '@angular/core'; import {NgStyle} from '@angular/common'; -import {Resizable, ResizeEvent, Edges} from './../angular2-resizable'; +import {Resizable, ResizeEvent, Edges, ResizeHandle} from './../angular2-resizable'; import { describe, expect, @@ -18,7 +18,7 @@ import { describe('resizable directive', () => { @Component({ - directives: [Resizable, NgStyle], + directives: [Resizable, NgStyle, ResizeHandle], styles: [` .rectangle { position: relative; @@ -57,29 +57,34 @@ describe('resizable directive', () => { } - const triggerDomEvent: Function = (eventType: string, target: HTMLElement, eventData: Object = {}) => { + const triggerDomEvent: Function = (eventType: string, target: HTMLElement | Element, eventData: Object = {}) => { const event: Event = document.createEvent('Event'); Object.assign(event, eventData); event.initEvent(eventType, true, true); target.dispatchEvent(event); }; - let builder: TestComponentBuilder, componentPromise: Promise>; - beforeEach(inject([TestComponentBuilder], (tcb) => { - builder = tcb; + let componentPromise: Promise>, createComponent: Function; + beforeEach(inject([TestComponentBuilder], (builder) => { document.body.style.margin = '0px'; - componentPromise = builder.createAsync(TestCmp).then((fixture: ComponentFixture) => { - fixture.detectChanges(); - document.body.appendChild(fixture.componentInstance.resizable.elm.nativeElement); - return fixture; - }); + createComponent = (template?: String) => { + const componentBuilder: TestComponentBuilder = template ? builder.overrideTemplate(TestCmp, template) : builder; + componentPromise = componentBuilder.createAsync(TestCmp).then((fixture: ComponentFixture) => { + fixture.detectChanges(); + document.body.appendChild(fixture.componentInstance.resizable.elm.nativeElement); + return fixture; + }); + return componentPromise; + }; })); afterEach(async(() => { - componentPromise.then((fixture: ComponentFixture) => { - fixture.destroy(); - document.body.innerHTML = ''; - }); + if (componentPromise) { + componentPromise.then((fixture: ComponentFixture) => { + fixture.destroy(); + document.body.innerHTML = ''; + }); + } })); describe('cursor changes', () => { @@ -183,7 +188,7 @@ describe('resizable directive', () => { }); afterEach(async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; assertions.forEach(({coords, cursor}: {coords: Object, cursor: string}) => { triggerDomEvent('mousemove', elm, coords); @@ -469,7 +474,7 @@ describe('resizable directive', () => { }); afterEach(async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; domEvents.forEach(event => { triggerDomEvent(event.name, elm, event.data); @@ -490,7 +495,7 @@ describe('resizable directive', () => { it('should not resize when clicking and dragging outside of the element edges', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 10, @@ -517,7 +522,7 @@ describe('resizable directive', () => { it('should cancel an existing resize event', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 100, @@ -592,7 +597,7 @@ describe('resizable directive', () => { it('should reset existing styles after a resize', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 100, @@ -627,7 +632,7 @@ describe('resizable directive', () => { })); it('should cancel the mousedrag observable when the mouseup event fires', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 100, @@ -651,7 +656,7 @@ describe('resizable directive', () => { })); it('should fire the resize end event with the last valid bounding rectangle', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 100, @@ -682,7 +687,7 @@ describe('resizable directive', () => { })); it('should allow invalidating of resize events', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; triggerDomEvent('mousedown', elm, { clientX: 100, @@ -740,7 +745,7 @@ describe('resizable directive', () => { it('should only allow resizing of the element along the left side', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; fixture.componentInstance.resizeEdges = {left: true}; fixture.detectChanges(); @@ -772,7 +777,7 @@ describe('resizable directive', () => { it('should disable resizing of the element', async(() => { - componentPromise.then((fixture: ComponentFixture) => { + createComponent().then((fixture: ComponentFixture) => { const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; fixture.componentInstance.resizeEdges = {}; fixture.detectChanges(); @@ -800,4 +805,83 @@ describe('resizable directive', () => { })); + it('should support drag handles to resize the element', async(() => { + + createComponent(` +
+ + +
+ `).then((fixture: ComponentFixture) => { + + const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; + triggerDomEvent('mousedown', elm.querySelector('.resize-handle'), { + clientX: 395, + clientY: 345 + }); + expect(fixture.componentInstance.onResizeStart).toHaveBeenCalledWith({ + edges: { + bottom: true, + right: true + }, + rectangle: { + top: 200, + left: 100, + width: 300, + height: 150, + right: 400, + bottom: 350 + } + }); + triggerDomEvent('mousemove', elm.querySelector('.resize-handle'), { + clientX: 396, + clientY: 345 + }); + expect(fixture.componentInstance.onResize).toHaveBeenCalledWith({ + edges: { + bottom: true, + right: true + }, + rectangle: { + top: 200, + left: 100, + width: 301, + height: 150, + right: 401, + bottom: 350 + } + }); + triggerDomEvent('mouseup', elm.querySelector('.resize-handle'), { + clientX: 396, + clientY: 345 + }); + expect(fixture.componentInstance.onResizeEnd).toHaveBeenCalledWith({ + edges: { + bottom: true, + right: true + }, + rectangle: { + top: 200, + left: 100, + width: 301, + height: 150, + right: 401, + bottom: 350 + } + }); + + }); + + })); + });