ipsos-components
Version:
Material Design components for Angular
408 lines (352 loc) • 13.4 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 {Directionality} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ESCAPE} from '@angular/cdk/keycodes';
import {
Overlay,
OverlayConfig,
OverlayRef,
PositionStrategy,
RepositionScrollStrategy,
ScrollStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {take} from 'rxjs/operators/take';
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ComponentRef,
EventEmitter,
Inject,
InjectionToken,
Input,
NgZone,
OnDestroy,
Optional,
Output,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {DateAdapter} from '@angular/material/core';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {DOCUMENT} from '@angular/common';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {MatCalendar} from './calendar';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
/** Injection token that determines the scroll handling while the calendar is open. */
export const MAT_DATEPICKER_SCROLL_STRATEGY =
new InjectionToken<() => ScrollStrategy>('mat-datepicker-scroll-strategy');
/** @docs-private */
export function MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay):
() => RepositionScrollStrategy {
return () => overlay.scrollStrategies.reposition();
}
/** @docs-private */
export const MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER = {
provide: MAT_DATEPICKER_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER_FACTORY,
};
/**
* Component used as the content for the datepicker dialog and popup. We use this instead of using
* MatCalendar directly as the content so we can control the initial focus. This also gives us a
* place to put additional features of the popup that are not part of the calendar itself in the
* future. (e.g. confirmation buttons).
* @docs-private
*/
export class MatDatepickerContent<D> implements AfterContentInit {
datepicker: MatDatepicker<D>;
_calendar: MatCalendar<D>;
ngAfterContentInit() {
this._calendar._focusActiveCell();
}
/**
* Handles keydown event on datepicker content.
* @param event The event.
*/
_handleKeydown(event: KeyboardEvent): void {
if (event.keyCode === ESCAPE) {
this.datepicker.close();
event.preventDefault();
event.stopPropagation();
}
}
}
// TODO(mmalerba): We use a component instead of a directive here so the user can use implicit
// template reference variables (e.g. #d vs #d="matDatepicker"). We can change this to a directive
// if angular adds support for `exportAs: '$implicit'` on directives.
/** Component responsible for managing the datepicker popup/dialog. */
export class MatDatepicker<D> implements OnDestroy {
/** The date to open the calendar to initially. */
get startAt(): D | null {
// If an explicit startAt is set we start there, otherwise we start at whatever the currently
// selected value is.
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
}
set startAt(date: D | null) {
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(date));
}
private _startAt: D | null;
/** The view that the calendar should start in. */
startView: 'month' | 'year' = 'month';
/**
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
* than a popup and elements have more padding to allow for bigger touch targets.
*/
get touchUi(): boolean {
return this._touchUi;
}
set touchUi(value: boolean) {
this._touchUi = coerceBooleanProperty(value);
}
private _touchUi = false;
/** Whether the datepicker pop-up should be disabled. */
get disabled(): boolean {
return this._disabled === undefined && this._datepickerInput ?
this._datepickerInput.disabled : !!this._disabled;
}
set disabled(value: boolean) {
const newValue = coerceBooleanProperty(value);
if (newValue !== this._disabled) {
this._disabled = newValue;
this._disabledChange.next(newValue);
}
}
private _disabled: boolean;
/**
* Emits new selected date when selected date changes.
* @deprecated Switch to the `dateChange` and `dateInput` binding on the input element.
*/
selectedChanged = new EventEmitter<D>();
/** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */
panelClass: string | string[];
/** Emits when the datepicker has been opened. */
openedStream: EventEmitter<void> = new EventEmitter<void>();
/** Emits when the datepicker has been closed. */
closedStream: EventEmitter<void> = new EventEmitter<void>();
/** Whether the calendar is open. */
get opened(): boolean { return this._opened; }
set opened(shouldOpen: boolean) { shouldOpen ? this.open() : this.close(); }
private _opened = false;
/** The id for the datepicker calendar. */
id = `mat-datepicker-${datepickerUid++}`;
/** The currently selected date. */
get _selected(): D | null { return this._validSelected; }
set _selected(value: D | null) { this._validSelected = value; }
private _validSelected: D | null = null;
/** The minimum selectable date. */
get _minDate(): D | null {
return this._datepickerInput && this._datepickerInput.min;
}
/** The maximum selectable date. */
get _maxDate(): D | null {
return this._datepickerInput && this._datepickerInput.max;
}
get _dateFilter(): (date: D | null) => boolean {
return this._datepickerInput && this._datepickerInput._dateFilter;
}
/** A reference to the overlay when the calendar is opened as a popup. */
private _popupRef: OverlayRef;
/** A reference to the dialog when the calendar is opened as a dialog. */
private _dialogRef: MatDialogRef<any> | null;
/** A portal containing the calendar for this datepicker. */
private _calendarPortal: ComponentPortal<MatDatepickerContent<D>>;
/** The element that was focused before the datepicker was opened. */
private _focusedElementBeforeOpen: HTMLElement | null = null;
private _inputSubscription = Subscription.EMPTY;
/** The input element this datepicker is associated with. */
_datepickerInput: MatDatepickerInput<D>;
/** Emits when the datepicker is disabled. */
_disabledChange = new Subject<boolean>();
constructor(private _dialog: MatDialog,
private _overlay: Overlay,
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef,
private _scrollStrategy,
private _dateAdapter: DateAdapter<D>,
private _dir: Directionality,
private _document: any) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
}
ngOnDestroy() {
this.close();
this._inputSubscription.unsubscribe();
this._disabledChange.complete();
if (this._popupRef) {
this._popupRef.dispose();
}
}
/** Selects the given date */
_select(date: D): void {
let oldValue = this._selected;
this._selected = date;
if (!this._dateAdapter.sameDate(oldValue, this._selected)) {
this.selectedChanged.emit(date);
}
}
/**
* Register an input with this datepicker.
* @param input The datepicker input to register with this datepicker.
*/
_registerInput(input: MatDatepickerInput<D>): void {
if (this._datepickerInput) {
throw Error('A MatDatepicker can only be associated with a single input.');
}
this._datepickerInput = input;
this._inputSubscription =
this._datepickerInput._valueChange.subscribe((value: D | null) => this._selected = value);
}
/** Open the calendar. */
open(): void {
if (this._opened || this.disabled) {
return;
}
if (!this._datepickerInput) {
throw Error('Attempted to open an MatDatepicker with no associated input.');
}
if (this._document) {
this._focusedElementBeforeOpen = this._document.activeElement;
}
this.touchUi ? this._openAsDialog() : this._openAsPopup();
this._opened = true;
this.openedStream.emit();
}
/** Close the calendar. */
close(): void {
if (!this._opened) {
return;
}
if (this._popupRef && this._popupRef.hasAttached()) {
this._popupRef.detach();
}
if (this._dialogRef) {
this._dialogRef.close();
this._dialogRef = null;
}
if (this._calendarPortal && this._calendarPortal.isAttached) {
this._calendarPortal.detach();
}
if (this._focusedElementBeforeOpen &&
typeof this._focusedElementBeforeOpen.focus === 'function') {
this._focusedElementBeforeOpen.focus();
this._focusedElementBeforeOpen = null;
}
this._opened = false;
this.closedStream.emit();
}
/** Open the calendar as a dialog. */
private _openAsDialog(): void {
this._dialogRef = this._dialog.open(MatDatepickerContent, {
direction: this._dir ? this._dir.value : 'ltr',
viewContainerRef: this._viewContainerRef,
panelClass: 'mat-datepicker-dialog',
});
this._dialogRef.afterClosed().subscribe(() => this.close());
this._dialogRef.componentInstance.datepicker = this;
}
/** Open the calendar as a popup. */
private _openAsPopup(): void {
if (!this._calendarPortal) {
this._calendarPortal = new ComponentPortal(MatDatepickerContent, this._viewContainerRef);
}
if (!this._popupRef) {
this._createPopup();
}
if (!this._popupRef.hasAttached()) {
let componentRef: ComponentRef<MatDatepickerContent<D>> =
this._popupRef.attach(this._calendarPortal);
componentRef.instance.datepicker = this;
// Update the position once the calendar has rendered.
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this._popupRef.updatePosition();
});
}
this._popupRef.backdropClick().subscribe(() => this.close());
}
/** Create the popup. */
private _createPopup(): void {
const overlayConfig = new OverlayConfig({
positionStrategy: this._createPopupPositionStrategy(),
hasBackdrop: true,
backdropClass: 'mat-overlay-transparent-backdrop',
direction: this._dir ? this._dir.value : 'ltr',
scrollStrategy: this._scrollStrategy(),
panelClass: 'mat-datepicker-popup',
});
this._popupRef = this._overlay.create(overlayConfig);
}
/** Create the popup PositionStrategy. */
private _createPopupPositionStrategy(): PositionStrategy {
const fallbackOffset = this._datepickerInput._getPopupFallbackOffset();
return this._overlay.position()
.connectedTo(this._datepickerInput.getPopupConnectionElementRef(),
{originX: 'start', originY: 'bottom'},
{overlayX: 'start', overlayY: 'top'}
)
.withFallbackPosition(
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'bottom'},
undefined,
fallbackOffset
)
.withFallbackPosition(
{originX: 'end', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'}
)
.withFallbackPosition(
{originX: 'end', originY: 'top'},
{overlayX: 'end', overlayY: 'bottom'},
undefined,
fallbackOffset
);
}
/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
*/
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}
}