5b8c1623ddf94b417783467236d233ad46d83e43
[sdc/sdc-workflow-designer.git] /
1 /**
2  * @license
3  * Copyright Google Inc. All Rights Reserved.
4  *
5  * Use of this source code is governed by an MIT-style license that can be
6  * found in the LICENSE file at https://angular.io/license
7  */
8 /* tslint:disable:array-type member-access variable-name typedef
9  only-arrow-functions directive-class-suffix component-class-suffix
10  component-selector no-unnecessary-type-assertion arrow-parens*/
11 import {TemplatePortal} from '@angular/cdk';
12 import {Direction, Directionality} from '@angular/cdk';
13 import {ESCAPE} from '@angular/cdk';
14 import {Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Optional, Output, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
15 import {Subscription} from 'rxjs/Subscription';
16
17 import {Overlay} from './overlay';
18 import {OverlayRef} from './overlay-ref';
19 import {OverlayState} from './overlay-state';
20 import {ConnectedOverlayPositionChange, ConnectionPositionPair} from './position/connected-position';
21 import {ConnectedPositionStrategy} from './position/connected-position-strategy';
22 import {ScrollStrategy} from './scroll/scroll-strategy';
23
24 /** Coerces a data-bound value (typically a string) to a boolean. */
25 export function coerceBooleanProperty(value: any): boolean {
26   return value  !== null && `${value}` !== 'false';
27 }
28
29
30 /** Default set of positions for the overlay. Follows the behavior of a
31  * dropdown. */
32 const defaultPositionList = [
33   new ConnectionPositionPair(
34                 {originX: 'start', originY: 'bottom'},
35                 {overlayX: 'start', overlayY: 'top'}),
36   new ConnectionPositionPair(
37                 {originX: 'start', originY: 'top'},
38                 {overlayX: 'start', overlayY: 'bottom'}),
39 ];
40
41
42 /**
43  * Directive applied to an element to make it usable as an origin for an Overlay
44  * using a ConnectedPositionStrategy.
45  */
46 @Directive({
47   selector: '[nz-overlay-origin]',
48   exportAs: 'nzOverlayOrigin',
49 })
50 export class OverlayOrigin {
51   constructor(public elementRef: ElementRef) {}
52 }
53
54
55 /**
56  * Directive to facilitate declarative creation of an Overlay using a
57  * ConnectedPositionStrategy.
58  */
59 @Directive({selector: '[nz-connected-overlay]', exportAs: 'nzConnectedOverlay'})
60 export class ConnectedOverlayDirective implements OnDestroy, OnChanges {
61   private _overlayRef: OverlayRef;
62   private _templatePortal: TemplatePortal;
63   private _hasBackdrop = false;
64   private _backdropSubscription: Subscription|null;
65   private _positionSubscription: Subscription;
66   private _offsetX = 0;
67   private _offsetY = 0;
68   private _position: ConnectedPositionStrategy;
69   private _escapeListener: Function;
70
71   /** Origin for the connected overlay. */
72   @Input() origin: OverlayOrigin;
73
74   /** Registered connected position pairs. */
75   @Input() positions: ConnectionPositionPair[];
76
77   /** The offset in pixels for the overlay connection point on the x-axis */
78   @Input()
79   get offsetX(): number {
80         return this._offsetX;
81   }
82
83   set offsetX(offsetX: number) {
84         this._offsetX = offsetX;
85         if (this._position) {
86                 this._position.withOffsetX(offsetX);
87         }
88   }
89
90   /** The offset in pixels for the overlay connection point on the y-axis */
91   @Input()
92   get offsetY() {
93         return this._offsetY;
94   }
95
96   set offsetY(offsetY: number) {
97         this._offsetY = offsetY;
98         if (this._position) {
99                 this._position.withOffsetY(offsetY);
100         }
101   }
102
103   /** The width of the overlay panel. */
104   @Input() width: number|string;
105
106   /** The height of the overlay panel. */
107   @Input() height: number|string;
108
109   /** The min width of the overlay panel. */
110   @Input() minWidth: number|string;
111
112   /** The min height of the overlay panel. */
113   @Input() minHeight: number|string;
114
115   /** The custom class to be set on the backdrop element. */
116   @Input() backdropClass: string;
117
118   /** The custom class to be set on the pane element. */
119   @Input() paneClass: string;
120
121   /** Strategy to be used when handling scroll events while the overlay is open.
122    */
123   @Input()
124   scrollStrategy: ScrollStrategy = this._overlay.scrollStrategies.reposition();
125
126   /** Whether the overlay is open. */
127   @Input() open = false;
128
129   /** Whether or not the overlay should attach a backdrop. */
130   @Input()
131   get hasBackdrop() {
132         return this._hasBackdrop;
133   }
134
135   set hasBackdrop(value: any) {
136         this._hasBackdrop = coerceBooleanProperty(value);
137   }
138
139   /** Event emitted when the backdrop is clicked. */
140   @Output() backdropClick = new EventEmitter<void>();
141
142   /** Event emitted when the position has changed. */
143   @Output() positionChange = new EventEmitter<ConnectedOverlayPositionChange>();
144
145   /** Event emitted when the overlay has been attached. */
146   @Output() attach = new EventEmitter<void>();
147
148   /** Event emitted when the overlay has been detached. */
149   @Output() detach = new EventEmitter<void>();
150
151   // TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
152
153   constructor(
154                 private _overlay: Overlay, private _renderer: Renderer2,
155                 templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef,
156                 @Optional() private _dir: Directionality) {
157         this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
158   }
159
160   /** The associated overlay reference. */
161   get overlayRef(): OverlayRef {
162         return this._overlayRef;
163   }
164
165   /** The element's layout direction. */
166   get dir(): Direction {
167         return this._dir ? this._dir.value : 'ltr';
168   }
169
170   ngOnDestroy() {
171         this._destroyOverlay();
172   }
173
174   ngOnChanges(changes: SimpleChanges) {
175         if (changes['open']) {
176                 this.open ? this._attachOverlay() : this._detachOverlay();
177         }
178   }
179
180   /** Creates an overlay */
181   private _createOverlay() {
182         if (!this.positions || !this.positions.length) {
183                 this.positions = defaultPositionList;
184         }
185
186         this._overlayRef =
187                 this._overlay.create(this._buildConfig(), this.paneClass);
188   }
189
190   /** Builds the overlay config based on the directive's inputs */
191   private _buildConfig(): OverlayState {
192         const overlayConfig = new OverlayState();
193
194         if (this.width || this.width === 0) {
195                 overlayConfig.width = this.width;
196         }
197
198         if (this.height || this.height === 0) {
199                 overlayConfig.height = this.height;
200         }
201
202         if (this.minWidth || this.minWidth === 0) {
203                 overlayConfig.minWidth = this.minWidth;
204         }
205
206         if (this.minHeight || this.minHeight === 0) {
207                 overlayConfig.minHeight = this.minHeight;
208         }
209
210         overlayConfig.hasBackdrop = this.hasBackdrop;
211
212         if (this.backdropClass) {
213                 overlayConfig.backdropClass = this.backdropClass;
214         }
215
216         this._position =
217                 this._createPositionStrategy() as ConnectedPositionStrategy;
218         overlayConfig.positionStrategy = this._position;
219         overlayConfig.scrollStrategy = this.scrollStrategy;
220
221         return overlayConfig;
222   }
223
224   /** Returns the position strategy of the overlay to be set on the overlay
225    * config */
226   private _createPositionStrategy(): ConnectedPositionStrategy {
227         const pos = this.positions[0];
228         const originPoint = {originX: pos.originX, originY: pos.originY};
229         const overlayPoint = {overlayX: pos.overlayX, overlayY: pos.overlayY};
230
231         const strategy =
232                 this._overlay.position()
233                         .connectedTo(this.origin.elementRef, originPoint, overlayPoint)
234                         .withOffsetX(this.offsetX)
235                         .withOffsetY(this.offsetY);
236
237         this._handlePositionChanges(strategy);
238
239         return strategy;
240   }
241
242   private _handlePositionChanges(strategy: ConnectedPositionStrategy): void {
243         for (let i = 1; i < this.positions.length; i++) {
244                 strategy.withFallbackPosition(
245                         {
246                         originX: this.positions[i].originX,
247                         originY: this.positions[i].originY
248                         },
249                         {
250                         overlayX: this.positions[i].overlayX,
251                         overlayY: this.positions[i].overlayY
252                         });
253         }
254
255         this._positionSubscription = strategy.onPositionChange.subscribe(
256                 pos => this.positionChange.emit(pos));
257   }
258
259   /** Attaches the overlay and subscribes to backdrop clicks if backdrop exists
260    */
261   private _attachOverlay() {
262         if (!this._overlayRef) {
263                 this._createOverlay();
264         }
265
266         this._position.withDirection(this.dir);
267         this._overlayRef.getState().direction = this.dir;
268         this._initEscapeListener();
269
270         if (!this._overlayRef.hasAttached()) {
271                 this._overlayRef.attach(this._templatePortal);
272                 this.attach.emit();
273         }
274
275         if (this.hasBackdrop) {
276                 this._backdropSubscription =
277                         this._overlayRef.backdropClick().subscribe(() => {
278                         this.backdropClick.emit();
279                         });
280         }
281   }
282
283   /** Detaches the overlay and unsubscribes to backdrop clicks if backdrop
284    * exists */
285   private _detachOverlay() {
286         if (this._overlayRef) {
287                 this._overlayRef.detach();
288                 this.detach.emit();
289         }
290
291         if (this._backdropSubscription) {
292                 this._backdropSubscription.unsubscribe();
293                 this._backdropSubscription = null;
294         }
295
296         if (this._escapeListener) {
297                 this._escapeListener();
298         }
299   }
300
301   /** Destroys the overlay created by this directive. */
302   private _destroyOverlay() {
303         if (this._overlayRef) {
304                 this._overlayRef.dispose();
305         }
306
307         if (this._backdropSubscription) {
308                 this._backdropSubscription.unsubscribe();
309         }
310
311         if (this._positionSubscription) {
312                 this._positionSubscription.unsubscribe();
313         }
314
315         if (this._escapeListener) {
316                 this._escapeListener();
317         }
318   }
319
320   /** Sets the event listener that closes the overlay when pressing Escape. */
321   private _initEscapeListener() {
322         this._escapeListener =
323                 this._renderer.listen('document', 'keydown', (event: KeyboardEvent) => {
324                         if (event.keyCode === ESCAPE) {
325                         this._detachOverlay();
326                         }
327                 });
328   }
329 }