3 * Copyright Google Inc. All Rights Reserved.
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
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';
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';
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';
30 /** Default set of positions for the overlay. Follows the behavior of a
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'}),
43 * Directive applied to an element to make it usable as an origin for an Overlay
44 * using a ConnectedPositionStrategy.
47 selector: '[nz-overlay-origin]',
48 exportAs: 'nzOverlayOrigin',
50 export class OverlayOrigin {
51 constructor(public elementRef: ElementRef) {}
56 * Directive to facilitate declarative creation of an Overlay using a
57 * ConnectedPositionStrategy.
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;
68 private _position: ConnectedPositionStrategy;
69 private _escapeListener: Function;
71 /** Origin for the connected overlay. */
72 @Input() origin: OverlayOrigin;
74 /** Registered connected position pairs. */
75 @Input() positions: ConnectionPositionPair[];
77 /** The offset in pixels for the overlay connection point on the x-axis */
79 get offsetX(): number {
83 set offsetX(offsetX: number) {
84 this._offsetX = offsetX;
86 this._position.withOffsetX(offsetX);
90 /** The offset in pixels for the overlay connection point on the y-axis */
96 set offsetY(offsetY: number) {
97 this._offsetY = offsetY;
99 this._position.withOffsetY(offsetY);
103 /** The width of the overlay panel. */
104 @Input() width: number|string;
106 /** The height of the overlay panel. */
107 @Input() height: number|string;
109 /** The min width of the overlay panel. */
110 @Input() minWidth: number|string;
112 /** The min height of the overlay panel. */
113 @Input() minHeight: number|string;
115 /** The custom class to be set on the backdrop element. */
116 @Input() backdropClass: string;
118 /** The custom class to be set on the pane element. */
119 @Input() paneClass: string;
121 /** Strategy to be used when handling scroll events while the overlay is open.
124 scrollStrategy: ScrollStrategy = this._overlay.scrollStrategies.reposition();
126 /** Whether the overlay is open. */
127 @Input() open = false;
129 /** Whether or not the overlay should attach a backdrop. */
132 return this._hasBackdrop;
135 set hasBackdrop(value: any) {
136 this._hasBackdrop = coerceBooleanProperty(value);
139 /** Event emitted when the backdrop is clicked. */
140 @Output() backdropClick = new EventEmitter<void>();
142 /** Event emitted when the position has changed. */
143 @Output() positionChange = new EventEmitter<ConnectedOverlayPositionChange>();
145 /** Event emitted when the overlay has been attached. */
146 @Output() attach = new EventEmitter<void>();
148 /** Event emitted when the overlay has been detached. */
149 @Output() detach = new EventEmitter<void>();
151 // TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
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);
160 /** The associated overlay reference. */
161 get overlayRef(): OverlayRef {
162 return this._overlayRef;
165 /** The element's layout direction. */
166 get dir(): Direction {
167 return this._dir ? this._dir.value : 'ltr';
171 this._destroyOverlay();
174 ngOnChanges(changes: SimpleChanges) {
175 if (changes['open']) {
176 this.open ? this._attachOverlay() : this._detachOverlay();
180 /** Creates an overlay */
181 private _createOverlay() {
182 if (!this.positions || !this.positions.length) {
183 this.positions = defaultPositionList;
187 this._overlay.create(this._buildConfig(), this.paneClass);
190 /** Builds the overlay config based on the directive's inputs */
191 private _buildConfig(): OverlayState {
192 const overlayConfig = new OverlayState();
194 if (this.width || this.width === 0) {
195 overlayConfig.width = this.width;
198 if (this.height || this.height === 0) {
199 overlayConfig.height = this.height;
202 if (this.minWidth || this.minWidth === 0) {
203 overlayConfig.minWidth = this.minWidth;
206 if (this.minHeight || this.minHeight === 0) {
207 overlayConfig.minHeight = this.minHeight;
210 overlayConfig.hasBackdrop = this.hasBackdrop;
212 if (this.backdropClass) {
213 overlayConfig.backdropClass = this.backdropClass;
217 this._createPositionStrategy() as ConnectedPositionStrategy;
218 overlayConfig.positionStrategy = this._position;
219 overlayConfig.scrollStrategy = this.scrollStrategy;
221 return overlayConfig;
224 /** Returns the position strategy of the overlay to be set on the overlay
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};
232 this._overlay.position()
233 .connectedTo(this.origin.elementRef, originPoint, overlayPoint)
234 .withOffsetX(this.offsetX)
235 .withOffsetY(this.offsetY);
237 this._handlePositionChanges(strategy);
242 private _handlePositionChanges(strategy: ConnectedPositionStrategy): void {
243 for (let i = 1; i < this.positions.length; i++) {
244 strategy.withFallbackPosition(
246 originX: this.positions[i].originX,
247 originY: this.positions[i].originY
250 overlayX: this.positions[i].overlayX,
251 overlayY: this.positions[i].overlayY
255 this._positionSubscription = strategy.onPositionChange.subscribe(
256 pos => this.positionChange.emit(pos));
259 /** Attaches the overlay and subscribes to backdrop clicks if backdrop exists
261 private _attachOverlay() {
262 if (!this._overlayRef) {
263 this._createOverlay();
266 this._position.withDirection(this.dir);
267 this._overlayRef.getState().direction = this.dir;
268 this._initEscapeListener();
270 if (!this._overlayRef.hasAttached()) {
271 this._overlayRef.attach(this._templatePortal);
275 if (this.hasBackdrop) {
276 this._backdropSubscription =
277 this._overlayRef.backdropClick().subscribe(() => {
278 this.backdropClick.emit();
283 /** Detaches the overlay and unsubscribes to backdrop clicks if backdrop
285 private _detachOverlay() {
286 if (this._overlayRef) {
287 this._overlayRef.detach();
291 if (this._backdropSubscription) {
292 this._backdropSubscription.unsubscribe();
293 this._backdropSubscription = null;
296 if (this._escapeListener) {
297 this._escapeListener();
301 /** Destroys the overlay created by this directive. */
302 private _destroyOverlay() {
303 if (this._overlayRef) {
304 this._overlayRef.dispose();
307 if (this._backdropSubscription) {
308 this._backdropSubscription.unsubscribe();
311 if (this._positionSubscription) {
312 this._positionSubscription.unsubscribe();
315 if (this._escapeListener) {
316 this._escapeListener();
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();