|
| 1 | +import { Renderer2 } from '@angular/core'; |
| 2 | +import { MatDialogRef } from '@angular/material/dialog'; |
| 3 | +import { DragRef } from '@angular/cdk/drag-drop'; |
| 4 | +import { fromEvent } from 'rxjs/internal/observable/fromEvent'; |
| 5 | +import { Subscription } from 'rxjs'; |
| 6 | +import { merge } from 'rxjs'; |
| 7 | +import { Point } from '@angular/cdk/drag-drop/drag-ref'; |
| 8 | + |
| 9 | +enum corners { |
| 10 | + topRight = 'topRight', |
| 11 | + bottomRight = 'bottomRight', |
| 12 | + bottomLeft = 'bottomLeft', |
| 13 | + topLeft = 'topLeft', |
| 14 | +} |
| 15 | +enum cursors { |
| 16 | + nesw = 'nesw-resize', |
| 17 | + nwse = 'nwse-resize', |
| 18 | +} |
| 19 | +enum verticalAlignment { |
| 20 | + top = 'top', |
| 21 | + bottom = 'bottom', |
| 22 | +} |
| 23 | +enum horizontalAlignment { |
| 24 | + right = 'right', |
| 25 | + left = 'left', |
| 26 | +} |
| 27 | + |
| 28 | +const cornerWidth: string = '16px'; |
| 29 | +const offset: string = '0px'; |
| 30 | +const minWidth: number = 200; |
| 31 | +const minHeight: number = 200; |
| 32 | + |
| 33 | +function getPixels(sizeString: string): number { |
| 34 | + return parseFloat((sizeString || '').replace('px', '')); |
| 35 | +} |
| 36 | + |
| 37 | +function clamp(min: number, num: number, max: number): number { |
| 38 | + return Math.min(Math.max(num, min), max); |
| 39 | +} |
| 40 | + |
| 41 | +export class ResizableDraggableDialog { |
| 42 | + cornerElements: HTMLElement[] = []; |
| 43 | + pointerDownSubs: Subscription[] = []; |
| 44 | + |
| 45 | + constructor( |
| 46 | + private _document: any, |
| 47 | + private _renderer2: Renderer2, |
| 48 | + private _dialogRef: MatDialogRef<any>, |
| 49 | + private _dragRef: DragRef, |
| 50 | + ) { |
| 51 | + this._initialPositionReset(); |
| 52 | + this._attachCorners(); |
| 53 | + } |
| 54 | + |
| 55 | + public attach(): void { |
| 56 | + this.detach(); |
| 57 | + this._attachCorners(); |
| 58 | + } |
| 59 | + |
| 60 | + public detach(): void { |
| 61 | + this.pointerDownSubs.forEach((sub: Subscription) => sub.unsubscribe()); |
| 62 | + this.pointerDownSubs = []; |
| 63 | + this.cornerElements.forEach((elem: HTMLElement) => this._renderer2.removeChild(this._getDialogWrapper(), elem)); |
| 64 | + this.cornerElements = []; |
| 65 | + } |
| 66 | + |
| 67 | + private _getDialogWrapper(): HTMLElement { |
| 68 | + return (<HTMLElement>this._document.getElementById(this._dialogRef.id) || {}).parentElement; |
| 69 | + } |
| 70 | + |
| 71 | + private _getViewportDimensions(): ClientRect { |
| 72 | + return this._getDialogWrapper().parentElement.getBoundingClientRect(); |
| 73 | + } |
| 74 | + |
| 75 | + private _getDialogWrapperDimensions(): { width: number; height: number } { |
| 76 | + const dimensions: CSSStyleDeclaration = getComputedStyle(this._getDialogWrapper()); |
| 77 | + return { |
| 78 | + width: getPixels(dimensions.width), |
| 79 | + height: getPixels(dimensions.height), |
| 80 | + }; |
| 81 | + } |
| 82 | + |
| 83 | + private _initialPositionReset(): void { |
| 84 | + const { right: viewportWidth, bottom: viewportHeight }: ClientRect = this._getViewportDimensions(); |
| 85 | + const { width, height } = this._getDialogWrapperDimensions(); |
| 86 | + const { |
| 87 | + marginRight: originalDialogRight, |
| 88 | + marginLeft: originalDialogLeft, |
| 89 | + marginBottom: originalDialogBottom, |
| 90 | + marginTop: originalDialogTop, |
| 91 | + } = this._getDialogWrapper().style; |
| 92 | + let x: number; |
| 93 | + if (originalDialogLeft) { |
| 94 | + x = getPixels(originalDialogLeft); |
| 95 | + } else if (originalDialogRight) { |
| 96 | + x = viewportWidth - getPixels(originalDialogRight) - width; |
| 97 | + } else { |
| 98 | + x = (viewportWidth - width) / 2; |
| 99 | + } |
| 100 | + let y: number; |
| 101 | + if (originalDialogTop) { |
| 102 | + y = getPixels(originalDialogTop); |
| 103 | + } else if (originalDialogBottom) { |
| 104 | + y = viewportHeight - getPixels(originalDialogBottom) - height; |
| 105 | + } else { |
| 106 | + y = (viewportHeight - height) / 2; |
| 107 | + } |
| 108 | + // use drag ref's mechanisms for positioning instead of the dialog's |
| 109 | + this._dialogRef.updatePosition({ top: '0px', right: '0px', bottom: '0px', left: '0px' }); |
| 110 | + this._dragRef.setFreeDragPosition({ x, y }); |
| 111 | + } |
| 112 | + |
| 113 | + private _attachCorners(): void { |
| 114 | + Object.values(corners).forEach((corner: corners) => { |
| 115 | + const element: HTMLElement = this._renderer2.createElement('div'); |
| 116 | + this.cornerElements = [...this.cornerElements, element]; |
| 117 | + this._renderer2.setStyle(element, 'position', 'absolute'); |
| 118 | + this._renderer2.setStyle(element, 'width', cornerWidth); |
| 119 | + this._renderer2.setStyle(element, 'height', cornerWidth); |
| 120 | + this._renderer2.appendChild(this._getDialogWrapper(), element); |
| 121 | + |
| 122 | + let cursor: cursors; |
| 123 | + let topBottom: verticalAlignment; |
| 124 | + let rightLeft: horizontalAlignment; |
| 125 | + |
| 126 | + if (corner === corners.topRight) { |
| 127 | + cursor = cursors.nesw; |
| 128 | + topBottom = verticalAlignment.top; |
| 129 | + rightLeft = horizontalAlignment.right; |
| 130 | + } else if (corner === corners.bottomRight) { |
| 131 | + cursor = cursors.nwse; |
| 132 | + topBottom = verticalAlignment.bottom; |
| 133 | + rightLeft = horizontalAlignment.right; |
| 134 | + |
| 135 | + const icon: HTMLElement = this._renderer2.createElement('i'); |
| 136 | + this._renderer2.addClass(icon, 'material-icons'); |
| 137 | + this._renderer2.appendChild(icon, this._renderer2.createText('filter_list')); |
| 138 | + this._renderer2.appendChild(element, icon); |
| 139 | + this._renderer2.setStyle(icon, 'transform', `rotate(${315}deg) translate(0px, ${offset})`); |
| 140 | + this._renderer2.setStyle(icon, 'font-size', cornerWidth); |
| 141 | + } else if (corner === corners.bottomLeft) { |
| 142 | + cursor = cursors.nesw; |
| 143 | + topBottom = verticalAlignment.bottom; |
| 144 | + rightLeft = horizontalAlignment.left; |
| 145 | + } else if (corner === corners.topLeft) { |
| 146 | + cursor = cursors.nwse; |
| 147 | + topBottom = verticalAlignment.top; |
| 148 | + rightLeft = horizontalAlignment.left; |
| 149 | + } |
| 150 | + this._renderer2.setStyle(element, topBottom, offset); |
| 151 | + this._renderer2.setStyle(element, rightLeft, offset); |
| 152 | + this._renderer2.setStyle(element, 'cursor', cursor); |
| 153 | + |
| 154 | + const pointerDownSub: Subscription = fromEvent(element, 'pointerdown').subscribe((event: PointerEvent) => { |
| 155 | + this._handleMouseDown(event, corner); |
| 156 | + }); |
| 157 | + this.pointerDownSubs = [...this.pointerDownSubs, pointerDownSub]; |
| 158 | + }); |
| 159 | + } |
| 160 | + |
| 161 | + private _handleMouseDown(event: PointerEvent, corner: corners): void { |
| 162 | + const { width: originalWidth, height: originalHeight } = this._getDialogWrapperDimensions(); |
| 163 | + const originalMouseX: number = event.pageX; |
| 164 | + const originalMouseY: number = event.pageY; |
| 165 | + const { x: currentTransformX, y: currentTransformY }: Point = this._dragRef.getFreeDragPosition(); |
| 166 | + const { |
| 167 | + bottom: distanceFromBottom, |
| 168 | + right: distanceFromRight, |
| 169 | + }: ClientRect = this._getDialogWrapper().getBoundingClientRect(); |
| 170 | + const { right: viewportWidth, bottom: viewportHeight }: ClientRect = this._getViewportDimensions(); |
| 171 | + |
| 172 | + const mouseMoveSub: Subscription = fromEvent(window, 'pointermove').subscribe((e: PointerEvent) => { |
| 173 | + e.preventDefault(); // prevent highlighting of text when dragging |
| 174 | + |
| 175 | + const yDelta: number = clamp(0, e.pageY, viewportHeight) - originalMouseY; |
| 176 | + const xDelta: number = clamp(0, e.pageX, viewportWidth) - originalMouseX; |
| 177 | + let newHeight: number; |
| 178 | + let newWidth: number; |
| 179 | + let newTransformY: number = 0; |
| 180 | + let newTransformX: number = 0; |
| 181 | + |
| 182 | + // top right |
| 183 | + if (corner === corners.topRight) { |
| 184 | + newHeight = clamp(minHeight, originalHeight - yDelta, viewportHeight); |
| 185 | + newWidth = clamp(minWidth, originalWidth + xDelta, viewportWidth); |
| 186 | + newTransformY = clamp(0, currentTransformY + yDelta, distanceFromBottom - newHeight); |
| 187 | + newTransformX = currentTransformX; |
| 188 | + } |
| 189 | + // bottom right |
| 190 | + else if (corner === corners.bottomRight) { |
| 191 | + newHeight = clamp(minHeight, originalHeight + yDelta, viewportHeight); |
| 192 | + newWidth = clamp(minWidth, originalWidth + xDelta, viewportWidth); |
| 193 | + newTransformY = currentTransformY; |
| 194 | + newTransformX = currentTransformX; |
| 195 | + } |
| 196 | + // bottom left |
| 197 | + else if (corner === corners.bottomLeft) { |
| 198 | + newHeight = clamp(minHeight, originalHeight + yDelta, viewportHeight); |
| 199 | + newWidth = clamp(minWidth, originalWidth - xDelta, viewportWidth); |
| 200 | + newTransformY = currentTransformY; |
| 201 | + newTransformX = clamp(0, currentTransformX + xDelta, distanceFromRight - newWidth); |
| 202 | + } |
| 203 | + // top left |
| 204 | + else if (corner === corners.topLeft) { |
| 205 | + newHeight = clamp(minHeight, originalHeight - yDelta, viewportHeight); |
| 206 | + newWidth = clamp(minWidth, originalWidth - xDelta, viewportWidth); |
| 207 | + |
| 208 | + newTransformX = clamp(0, currentTransformX + xDelta, distanceFromRight - newWidth); |
| 209 | + newTransformY = clamp(0, currentTransformY + yDelta, distanceFromBottom - newHeight); |
| 210 | + } |
| 211 | + this._dialogRef.updateSize(`${newWidth}px`, `${newHeight}px`); |
| 212 | + this._dragRef.setFreeDragPosition({ |
| 213 | + x: newTransformX, |
| 214 | + y: newTransformY, |
| 215 | + }); |
| 216 | + }); |
| 217 | + |
| 218 | + const mouseUpSub: Subscription = merge( |
| 219 | + fromEvent(window, 'pointerup'), |
| 220 | + fromEvent(window, 'pointercancel'), |
| 221 | + ).subscribe(() => { |
| 222 | + mouseMoveSub.unsubscribe(); |
| 223 | + mouseUpSub.unsubscribe(); |
| 224 | + }); |
| 225 | + } |
| 226 | +} |
0 commit comments