From b77c9fc1cb94007386e4b6ff1c3d29aaea600a4a Mon Sep 17 00:00:00 2001 From: Badradine Boulahia Date: Thu, 16 Mar 2017 19:00:22 +0100 Subject: [PATCH] feat(ghostElementPositioning): allow ghost element positioning to be configured * feat(elementAbsolutePosition): using offsetPosition when true * feat(elementAbsolutePosition): using offsetPosition when true * feat(elementAbsolutePosition): using offsetPosition when true * feat(elementAbsolutePosition): add unit-test --- .gitignore | 2 + src/resizable.directive.ts | 28 ++++++- test/resizable.spec.ts | 159 ++++++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 370ca30..9dc6fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .DS_Store node_modules coverage + +*.iml diff --git a/src/resizable.directive.ts b/src/resizable.directive.ts index 820a597..9cd2d87 100644 --- a/src/resizable.directive.ts +++ b/src/resizable.directive.ts @@ -64,6 +64,25 @@ function getNewBoundingRectangle(startingRect: BoundingRectangle, edges: Edges, } +function getElementRect(element: ElementRef, ghostElementPositioning: string): BoundingRectangle { + if (ghostElementPositioning === 'absolute') { + return { + top: element.nativeElement.offsetTop, + bottom: element.nativeElement.offsetHeight + element.nativeElement.offsetTop, + left: element.nativeElement.offsetLeft, + right: element.nativeElement.offsetWidth + element.nativeElement.offsetLeft + }; + } else { + const boundingRect: BoundingRectangle = element.nativeElement.getBoundingClientRect(); + return { + top: boundingRect.top, + bottom: boundingRect.bottom, + left: boundingRect.left, + right: boundingRect.right + }; + } +} + function isWithinBoundingY({mouseY, rect}: {mouseY: number, rect: ClientRect}): boolean { return mouseY >= rect.top && mouseY <= rect.bottom; } @@ -212,6 +231,11 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit { */ @Input() resizeCursorPrecision: number = 3; + /** + * Define the positioning of the ghost element (can be fixed or absolute) + */ + @Input() ghostElementPositioning: 'fixed' | 'absolute' = 'fixed'; + /** * Called when the mouse is pressed and a resize event is about to begin. `$event` is a `ResizeEvent` object. */ @@ -408,7 +432,7 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit { if (currentResize) { removeGhostElement(); } - const startingRect: BoundingRectangle = this.elm.nativeElement.getBoundingClientRect(); + const startingRect: BoundingRectangle = getElementRect(this.elm, this.ghostElementPositioning); currentResize = { edges, startingRect, @@ -419,7 +443,7 @@ export class Resizable implements OnInit, OnDestroy, AfterViewInit { const resizeCursors: ResizeCursors = Object.assign({}, DEFAULT_RESIZE_CURSORS, this.resizeCursors); this.elm.nativeElement.parentElement.appendChild(currentResize.clonedNode); this.renderer.setElementStyle(this.elm.nativeElement, 'visibility', 'hidden'); - this.renderer.setElementStyle(currentResize.clonedNode, 'position', 'fixed'); + this.renderer.setElementStyle(currentResize.clonedNode, 'position', this.ghostElementPositioning); this.renderer.setElementStyle(currentResize.clonedNode, 'left', `${currentResize.startingRect.left}px`); this.renderer.setElementStyle(currentResize.clonedNode, 'top', `${currentResize.startingRect.top}px`); this.renderer.setElementStyle(currentResize.clonedNode, 'height', `${currentResize.startingRect.height}px`); diff --git a/test/resizable.spec.ts b/test/resizable.spec.ts index 4ac0af3..85e80ad 100644 --- a/test/resizable.spec.ts +++ b/test/resizable.spec.ts @@ -29,6 +29,7 @@ describe('resizable directive', () => { [resizeSnapGrid]="resizeSnapGrid" [resizeCursors]="resizeCursors" [resizeCursorPrecision]="resizeCursorPrecision" + [ghostElementPositioning]="ghostElementPositioning" (resizeStart)="resizeStart($event)" (resizing)="resizing($event)" (resizeEnd)="resizeEnd($event)"> @@ -48,7 +49,7 @@ describe('resizable directive', () => { public resizeSnapGrid: Object = {}; public resizeCursors: Object = {}; public resizeCursorPrecision: number; - + public ghostElementPositioning: 'fixed' | 'absolute' = 'fixed'; } const triggerDomEvent: Function = (eventType: string, target: HTMLElement | Element, eventData: Object = {}) => { @@ -65,9 +66,12 @@ describe('resizable directive', () => { let component: ComponentFixture, createComponent: Function; beforeEach(() => { document.body.style.margin = '0px'; - createComponent = (template?: string) => { + createComponent = (template?: string, styles?: Array) => { if (template) { - TestBed.overrideComponent(TestCmp, {set: {template}}); + TestBed.overrideComponent(TestCmp, {set: {template: template}}); + } + if (styles) { + TestBed.overrideComponent(TestCmp, {set: {styles: styles}}); } const fixture: ComponentFixture = TestBed.createComponent(TestCmp); fixture.detectChanges(); @@ -1077,4 +1081,153 @@ describe('resizable directive', () => { }); + describe('absolute positioning', () => { + let domEvents: Array; + beforeEach(() => { + domEvents = []; + }); + + it('should have the same top/left/height when resize from the right', () => { + domEvents.push({ + name: 'mousedown', + data: { + clientX: 600, + clientY: 405 + } + }); + domEvents.push({ + name: 'mousemove', + data: { + clientX: 620, + clientY: 405 + }, + style: { + top: '200px', + left: '100px', + height: '150px' + } + }); + }); + + it('should have the same top/height when resize from the left', () => { + domEvents.push({ + name: 'mousedown', + data: { + clientX: 300, + clientY: 405 + } + }); + domEvents.push({ + name: 'mousemove', + data: { + clientX: 280, + clientY: 405 + }, + style: { + top: '200px', + height: '150px' + } + }); + }); + + it('should have the same left/width when resize from the top', () => { + domEvents.push({ + name: 'mousedown', + data: { + clientX: 400, + clientY: 400 + } + }); + domEvents.push({ + name: 'mousemove', + data: { + clientX: 400, + clientY: 280 + }, + style: { + left: '100px', + width: '300px' + } + }); + }); + + it('should have the same top/left/width when resize from the bottom', () => { + domEvents.push({ + name: 'mousedown', + data: { + clientX: 400, + clientY: 550 + } + }); + domEvents.push({ + name: 'mousemove', + data: { + clientX: 400, + clientY: 570 + }, + style: { + top: '200px', + left: '100px', + width: '300px', + } + }); + }); + + afterEach(() => { + const template: string = ` +
+
+
+
+ `; + const styles: Array = [` + .container { + -webkit-transform: scale3d(1, 1, 1); + position: relative; + top: 200px; + left: 200px; + } + .rectangle { + position: absolute; + top: 200px; + left: 100px; + width: 300px; + height: 150px; + } + `]; + + const fixture: ComponentFixture = createComponent(template, styles); + fixture.componentInstance.ghostElementPositioning = 'absolute'; + fixture.detectChanges(); + + const elm: HTMLElement = fixture.componentInstance.resizable.elm.nativeElement; + domEvents.forEach(event => { + triggerDomEvent(event.name, elm, event.data); + + const clonedNode: Element = elm.parentElement.children[1]; + if (event.name !== 'mouseup') { + expect(clonedNode['style'].position).to.equal('absolute'); + } + if (event.style) { + Object.keys(event.style).forEach(styleKey => { + expect(clonedNode['style'][styleKey]).to.equal(event.style[styleKey]); + }); + } + }); + }); + }); + });