Skip to content

Commit 3385ef5

Browse files
christianmemijeEd Morales
authored andcommitted
feat(markdown-nav): add resizability feature (#1563)
* feat(markdown-nav): resizability * test(): fix close issues of markdown nav service * refactor(markdown-nav): rename input and outputs * feat(markdown-nav): disable toggle dock for now
1 parent 9a12955 commit 3385ef5

File tree

10 files changed

+399
-59
lines changed

10 files changed

+399
-59
lines changed

src/platform/core/dialogs/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './alert-dialog/alert-dialog.component';
44
export * from './confirm-dialog/confirm-dialog.component';
55
export * from './prompt-dialog/prompt-dialog.component';
66
export * from './services/dialog.service';
7+
export * from './resizable-draggable-dialog/resizable-draggable-dialog';
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
}

src/platform/core/dialogs/services/dialog.service.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
import {
2-
Injectable,
3-
ViewContainerRef,
4-
Provider,
5-
SkipSelf,
6-
Optional,
7-
Inject,
8-
Renderer2,
9-
RendererFactory2,
10-
} from '@angular/core';
1+
import { Injectable, Inject, Renderer2, RendererFactory2 } from '@angular/core';
112
import { MatDialog, MatDialogRef, MatDialogConfig } from '@angular/material/dialog';
12-
import { ComponentType, TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
3+
import { ComponentType } from '@angular/cdk/portal';
134

145
import { TdAlertDialogComponent } from '../alert-dialog/alert-dialog.component';
156
import { TdConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component';
167
import { TdPromptDialogComponent } from '../prompt-dialog/prompt-dialog.component';
178
import { DragDrop, DragRef } from '@angular/cdk/drag-drop';
189
import { DOCUMENT } from '@angular/common';
1910
import { CovalentDialogsModule } from '../dialogs.module';
11+
import { Subject } from 'rxjs';
12+
2013
export interface IDialogConfig extends MatDialogConfig {
2114
title?: string;
2215
message: string;
@@ -44,6 +37,11 @@ export interface IDraggableConfig<T> {
4437
draggableClass?: string;
4538
}
4639

40+
export interface IDraggableRefs<T> {
41+
matDialogRef: MatDialogRef<T>;
42+
dragRefSubject: Subject<DragRef>;
43+
}
44+
4745
@Injectable({
4846
providedIn: CovalentDialogsModule,
4947
})
@@ -177,21 +175,22 @@ export class TdDialogService {
177175
config,
178176
dragHandleSelectors,
179177
draggableClass,
180-
}: IDraggableConfig<T>): MatDialogRef<T> {
181-
const dialogRef: MatDialogRef<T, any> = this._dialogService.open(component, config);
178+
}: IDraggableConfig<T>): IDraggableRefs<T> {
179+
const matDialogRef: MatDialogRef<T, any> = this._dialogService.open(component, config);
180+
181+
const dragRefSubject: Subject<DragRef> = new Subject<DragRef>();
182182

183183
const CDK_OVERLAY_PANE_SELECTOR: string = '.cdk-overlay-pane';
184184
const CDK_OVERLAY_CONTAINER_SELECTOR: string = '.cdk-overlay-container';
185185

186-
dialogRef.afterOpened().subscribe(() => {
187-
const dialogElement: HTMLElement = <HTMLElement>this._document.getElementById(dialogRef.id);
186+
matDialogRef.afterOpened().subscribe(() => {
187+
const dialogElement: HTMLElement = <HTMLElement>this._document.getElementById(matDialogRef.id);
188188
const draggableElement: DragRef = this._dragDrop.createDrag(dialogElement);
189189

190190
if (draggableClass) {
191191
const childComponent: Element = dialogElement.firstElementChild;
192192
this._renderer2.addClass(childComponent, draggableClass);
193193
}
194-
195194
if (dragHandleSelectors && dragHandleSelectors.length) {
196195
const dragHandles: Element[] = dragHandleSelectors.reduce(
197196
(acc: Element[], curr: string) => [...acc, ...Array.from(dialogElement.querySelectorAll(curr))],
@@ -201,18 +200,19 @@ export class TdDialogService {
201200
draggableElement.withHandles(<HTMLElement[]>dragHandles);
202201
}
203202
}
204-
205203
const rootElement: Element = dialogElement.closest(CDK_OVERLAY_PANE_SELECTOR);
206204
if (rootElement) {
207205
draggableElement.withRootElement(<HTMLElement>rootElement);
208206
}
207+
209208
const boundaryElement: Element = dialogElement.closest(CDK_OVERLAY_CONTAINER_SELECTOR);
210209
if (boundaryElement) {
211210
draggableElement.withBoundaryElement(<HTMLElement>boundaryElement);
212211
}
212+
dragRefSubject.next(draggableElement);
213213
});
214214

215-
return dialogRef;
215+
return { matDialogRef, dragRefSubject };
216216
}
217217

218218
private _createConfig(config: IDialogConfig): MatDialogConfig {

src/platform/markdown-navigator/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ A component that contains a MarkdownNavigator component and a toolbar
8484
+ toolbarColor?: ThemePalette
8585
+ Toolbar color
8686
+ Defaults to 'primary'
87+
+ docked?: boolean
88+
+ Whether docked or not.
89+
+ Defaults to false
90+
91+
#### Outputs
92+
93+
+ closed: void
94+
+ Event emitted when the close button is clicked.
95+
+ dockToggled: boolean
96+
+ Event emitted when the toggle dock state button is clicked.
97+
+ Emits current docked state.
8798

8899
## Setup
89100

0 commit comments

Comments
 (0)