ipsos-components
Version:
Material Design components for Angular
386 lines (317 loc) • 13.1 kB
text/typescript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Direction, Directionality} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ESCAPE} from '@angular/cdk/keycodes';
import {TemplatePortal} from '@angular/cdk/portal';
import {
Directive,
ElementRef,
EventEmitter,
Inject,
InjectionToken,
Input,
OnChanges,
OnDestroy,
Optional,
Output,
SimpleChanges,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {Overlay} from './overlay';
import {OverlayConfig} from './overlay-config';
import {OverlayRef} from './overlay-ref';
import {
ConnectedOverlayPositionChange,
ConnectionPositionPair,
} from './position/connected-position';
import {ConnectedPositionStrategy} from './position/connected-position-strategy';
import {RepositionScrollStrategy, ScrollStrategy} from './scroll/index';
import {DOCUMENT} from '@angular/common';
/** Default set of positions for the overlay. Follows the behavior of a dropdown. */
const defaultPositionList = [
new ConnectionPositionPair(
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'}),
new ConnectionPositionPair(
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'bottom'}),
new ConnectionPositionPair(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'}),
new ConnectionPositionPair(
{originX: 'end', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'}),
];
/** Injection token that determines the scroll handling while the connected overlay is open. */
export const CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY =
new InjectionToken<() => ScrollStrategy>('cdk-connected-overlay-scroll-strategy');
/** @docs-private */
export function CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay):
() => RepositionScrollStrategy {
return () => overlay.scrollStrategies.reposition();
}
/** @docs-private */
export const CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER = {
provide: CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER_FACTORY,
};
/**
* Directive applied to an element to make it usable as an origin for an Overlay using a
* ConnectedPositionStrategy.
*/
export class CdkOverlayOrigin {
constructor(
/** Reference to the element on which the directive is applied. */
public elementRef: ElementRef) { }
}
/**
* Directive to facilitate declarative creation of an Overlay using a ConnectedPositionStrategy.
*/
export class CdkConnectedOverlay implements OnDestroy, OnChanges {
private _overlayRef: OverlayRef;
private _templatePortal: TemplatePortal<any>;
private _hasBackdrop = false;
private _backdropSubscription = Subscription.EMPTY;
private _positionSubscription = Subscription.EMPTY;
private _offsetX: number = 0;
private _offsetY: number = 0;
private _position: ConnectedPositionStrategy;
/** Origin for the connected overlay. */
origin: CdkOverlayOrigin;
/** Registered connected position pairs. */
positions: ConnectionPositionPair[];
/** The offset in pixels for the overlay connection point on the x-axis */
get offsetX(): number { return this._offsetX; }
set offsetX(offsetX: number) {
this._offsetX = offsetX;
if (this._position) {
this._position.withOffsetX(offsetX);
}
}
/** The offset in pixels for the overlay connection point on the y-axis */
get offsetY() { return this._offsetY; }
set offsetY(offsetY: number) {
this._offsetY = offsetY;
if (this._position) {
this._position.withOffsetY(offsetY);
}
}
/** The width of the overlay panel. */
width: number | string;
/** The height of the overlay panel. */
height: number | string;
/** The min width of the overlay panel. */
minWidth: number | string;
/** The min height of the overlay panel. */
minHeight: number | string;
/** The custom class to be set on the backdrop element. */
backdropClass: string;
/** Strategy to be used when handling scroll events while the overlay is open. */
scrollStrategy: ScrollStrategy =
this._scrollStrategy();
/** Whether the overlay is open. */
open: boolean = false;
/** Whether or not the overlay should attach a backdrop. */
get hasBackdrop() { return this._hasBackdrop; }
set hasBackdrop(value: any) { this._hasBackdrop = coerceBooleanProperty(value); }
/** @deprecated */
get _deprecatedOrigin(): CdkOverlayOrigin { return this.origin; }
set _deprecatedOrigin(_origin: CdkOverlayOrigin) { this.origin = _origin; }
/** @deprecated */
get _deprecatedPositions(): ConnectionPositionPair[] { return this.positions; }
set _deprecatedPositions(_positions: ConnectionPositionPair[]) { this.positions = _positions; }
/** @deprecated */
get _deprecatedOffsetX(): number { return this.offsetX; }
set _deprecatedOffsetX(_offsetX: number) { this.offsetX = _offsetX; }
/** @deprecated */
get _deprecatedOffsetY(): number { return this.offsetY; }
set _deprecatedOffsetY(_offsetY: number) { this.offsetY = _offsetY; }
/** @deprecated */
get _deprecatedWidth(): number | string { return this.width; }
set _deprecatedWidth(_width: number | string) { this.width = _width; }
/** @deprecated */
get _deprecatedHeight(): number | string { return this.height; }
set _deprecatedHeight(_height: number | string) { this.height = _height; }
/** @deprecated */
get _deprecatedMinWidth(): number | string { return this.minWidth; }
set _deprecatedMinWidth(_minWidth: number | string) { this.minWidth = _minWidth; }
/** @deprecated */
get _deprecatedMinHeight(): number | string { return this.minHeight; }
set _deprecatedMinHeight(_minHeight: number | string) { this.minHeight = _minHeight; }
/** @deprecated */
get _deprecatedBackdropClass(): string { return this.backdropClass; }
set _deprecatedBackdropClass(_backdropClass: string) { this.backdropClass = _backdropClass; }
/** @deprecated */
get _deprecatedScrollStrategy(): ScrollStrategy { return this.scrollStrategy; }
set _deprecatedScrollStrategy(_scrollStrategy: ScrollStrategy) {
this.scrollStrategy = _scrollStrategy;
}
/** @deprecated */
get _deprecatedOpen(): boolean { return this.open; }
set _deprecatedOpen(_open: boolean) { this.open = _open; }
/** @deprecated */
get _deprecatedHasBackdrop() { return this.hasBackdrop; }
set _deprecatedHasBackdrop(_hasBackdrop: any) { this.hasBackdrop = _hasBackdrop; }
/** Event emitted when the backdrop is clicked. */
backdropClick = new EventEmitter<void>();
/** Event emitted when the position has changed. */
positionChange = new EventEmitter<ConnectedOverlayPositionChange>();
/** Event emitted when the overlay has been attached. */
attach = new EventEmitter<void>();
/** Event emitted when the overlay has been detached. */
detach = new EventEmitter<void>();
// TODO(jelbourn): inputs for size, scroll behavior, animation, etc.
constructor(
private _overlay: Overlay,
templateRef: TemplateRef<any>,
viewContainerRef: ViewContainerRef,
private _scrollStrategy,
private _dir: Directionality,
private _document: any) {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
}
/** The associated overlay reference. */
get overlayRef(): OverlayRef {
return this._overlayRef;
}
/** The element's layout direction. */
get dir(): Direction {
return this._dir ? this._dir.value : 'ltr';
}
ngOnDestroy() {
this._destroyOverlay();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['open'] || changes['_deprecatedOpen']) {
this.open ? this._attachOverlay() : this._detachOverlay();
}
}
/** Creates an overlay */
private _createOverlay() {
if (!this.positions || !this.positions.length) {
this.positions = defaultPositionList;
}
this._overlayRef = this._overlay.create(this._buildConfig());
}
/** Builds the overlay config based on the directive's inputs */
private _buildConfig(): OverlayConfig {
const positionStrategy = this._position = this._createPositionStrategy();
const overlayConfig = new OverlayConfig({
positionStrategy,
scrollStrategy: this.scrollStrategy,
hasBackdrop: this.hasBackdrop
});
if (this.width || this.width === 0) {
overlayConfig.width = this.width;
}
if (this.height || this.height === 0) {
overlayConfig.height = this.height;
}
if (this.minWidth || this.minWidth === 0) {
overlayConfig.minWidth = this.minWidth;
}
if (this.minHeight || this.minHeight === 0) {
overlayConfig.minHeight = this.minHeight;
}
if (this.backdropClass) {
overlayConfig.backdropClass = this.backdropClass;
}
return overlayConfig;
}
/** Returns the position strategy of the overlay to be set on the overlay config */
private _createPositionStrategy(): ConnectedPositionStrategy {
const pos = this.positions[0];
const originPoint = {originX: pos.originX, originY: pos.originY};
const overlayPoint = {overlayX: pos.overlayX, overlayY: pos.overlayY};
const strategy = this._overlay.position()
.connectedTo(this.origin.elementRef, originPoint, overlayPoint)
.withOffsetX(this.offsetX)
.withOffsetY(this.offsetY);
this._handlePositionChanges(strategy);
return strategy;
}
private _handlePositionChanges(strategy: ConnectedPositionStrategy): void {
for (let i = 1; i < this.positions.length; i++) {
strategy.withFallbackPosition(
{originX: this.positions[i].originX, originY: this.positions[i].originY},
{overlayX: this.positions[i].overlayX, overlayY: this.positions[i].overlayY}
);
}
this._positionSubscription =
strategy.onPositionChange.subscribe(pos => this.positionChange.emit(pos));
}
/** Attaches the overlay and subscribes to backdrop clicks if backdrop exists */
private _attachOverlay() {
if (!this._overlayRef) {
this._createOverlay();
}
this._position.withDirection(this.dir);
this._overlayRef.setDirection(this.dir);
this._document.addEventListener('keydown', this._escapeListener);
if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._templatePortal);
this.attach.emit();
}
if (this.hasBackdrop) {
this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => {
this.backdropClick.emit();
});
}
}
/** Detaches the overlay and unsubscribes to backdrop clicks if backdrop exists */
private _detachOverlay() {
if (this._overlayRef) {
this._overlayRef.detach();
this.detach.emit();
}
this._backdropSubscription.unsubscribe();
this._document.removeEventListener('keydown', this._escapeListener);
}
/** Destroys the overlay created by this directive. */
private _destroyOverlay() {
if (this._overlayRef) {
this._overlayRef.dispose();
}
this._backdropSubscription.unsubscribe();
this._positionSubscription.unsubscribe();
this._document.removeEventListener('keydown', this._escapeListener);
}
/** Event listener that will close the overlay when the user presses escape. */
private _escapeListener = (event: KeyboardEvent) => {
if (event.keyCode === ESCAPE) {
this._detachOverlay();
}
}
}