@ng-matero/extensions
Version:
Angular Material Extensions
714 lines (707 loc) • 239 kB
JavaScript
import { A11yModule } from '@angular/cdk/a11y';
import * as i1$1 from '@angular/cdk/overlay';
import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
import { ComponentPortal, CdkPortalOutlet, TemplatePortal, PortalModule } from '@angular/cdk/portal';
import { DOCUMENT, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { EventEmitter, booleanAttribute, Component, ViewEncapsulation, ChangeDetectionStrategy, Inject, Input, Output, Optional, Injectable, Directive, ElementRef, ViewChild, inject, Injector, afterNextRender, InjectionToken, forwardRef, Attribute, ContentChild, TemplateRef, NgModule } from '@angular/core';
import { MatButton, MatIconButton, MatButtonModule } from '@angular/material/button';
import { UP_ARROW, DOWN_ARROW, ENTER, PAGE_DOWN, PAGE_UP, END, HOME, RIGHT_ARROW, LEFT_ARROW, ESCAPE, hasModifierKey } from '@angular/cdk/keycodes';
import * as i1 from '@ng-matero/extensions/core';
import { MTX_DATETIME_FORMATS } from '@ng-matero/extensions/core';
import { normalizePassiveListenerOptions, _getFocusedElementPierceShadowDom } from '@angular/cdk/platform';
import { trigger, transition, animate, keyframes, style, state } from '@angular/animations';
import { coerceNumberProperty, coerceStringArray } from '@angular/cdk/coercion';
import { Subject, Subscription, merge, of } from 'rxjs';
import { take, filter } from 'rxjs/operators';
import * as i3 from '@angular/cdk/bidi';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, Validators } from '@angular/forms';
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';
import * as i2 from '@angular/material/form-field';
var MtxDatetimepickerFilterType;
(function (MtxDatetimepickerFilterType) {
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["DATE"] = 0] = "DATE";
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["HOUR"] = 1] = "HOUR";
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["MINUTE"] = 2] = "MINUTE";
})(MtxDatetimepickerFilterType || (MtxDatetimepickerFilterType = {}));
const activeEventOptions = normalizePassiveListenerOptions({ passive: false });
const CLOCK_RADIUS = 50;
const CLOCK_INNER_RADIUS = 27.5;
const CLOCK_OUTER_RADIUS = 41.25;
const CLOCK_TICK_RADIUS = 7.0833;
/**
* A clock that is used as part of the datetimepicker.
* @docs-private
*/
class MtxClock {
constructor(_elementRef, _adapter, _changeDetectorRef, _document) {
this._elementRef = _elementRef;
this._adapter = _adapter;
this._changeDetectorRef = _changeDetectorRef;
this._document = _document;
/** Step over minutes. */
this.interval = 1;
/** Whether the clock uses 12 hour format. */
this.twelvehour = false;
/** Whether the time is now in AM or PM. */
this.AMPM = 'AM';
/** Emits when the currently selected date changes. */
this.selectedChange = new EventEmitter();
/** Emits when any date is activated. */
this.activeDateChange = new EventEmitter();
/** Emits when any date is selected. */
this._userSelection = new EventEmitter();
/** Whether the clock is in hour view. */
this._hourView = true;
this._hours = [];
this._minutes = [];
this._timeChanged = false;
/** Called when the user has put their pointer down on the clock. */
this._pointerDown = (event) => {
this._timeChanged = false;
this.setTime(event);
this._bindGlobalEvents(event);
};
/**
* Called when the user has moved their pointer after
* starting to drag. Bound on the document level.
*/
this._pointerMove = (event) => {
if (event.cancelable) {
event.preventDefault();
}
this.setTime(event);
};
/** Called when the user has lifted their pointer. Bound on the document level. */
this._pointerUp = (event) => {
if (event.cancelable) {
event.preventDefault();
}
this._removeGlobalEvents();
if (this._timeChanged) {
this.selectedChange.emit(this.activeDate);
if (!this._hourView) {
this._userSelection.emit();
}
}
};
}
/**
* The date to display in this clock view.
*/
get activeDate() {
return this._activeDate;
}
set activeDate(value) {
const oldActiveDate = this._activeDate;
this._activeDate = this._adapter.clampDate(value, this.minDate, this.maxDate);
if (!this._adapter.sameMinute(oldActiveDate, this._activeDate)) {
this._init();
}
}
/** The currently selected date. */
get selected() {
return this._selected;
}
set selected(value) {
this._selected = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));
if (this._selected) {
this.activeDate = this._selected;
}
}
/** The minimum selectable date. */
get minDate() {
return this._minDate;
}
set minDate(value) {
this._minDate = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));
}
/** The maximum selectable date. */
get maxDate() {
return this._maxDate;
}
set maxDate(value) {
this._maxDate = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));
}
/** Whether the clock should be started in hour or minute view. */
set startView(value) {
this._hourView = value !== 'minute';
}
get _hand() {
const hour = this._adapter.getHour(this.activeDate);
this._selectedHour = hour;
this._selectedMinute = this._adapter.getMinute(this.activeDate);
let deg = 0;
let radius = CLOCK_OUTER_RADIUS;
if (this._hourView) {
const outer = this._selectedHour > 0 && this._selectedHour < 13;
radius = outer ? CLOCK_OUTER_RADIUS : CLOCK_INNER_RADIUS;
if (this.twelvehour) {
radius = CLOCK_OUTER_RADIUS;
}
deg = Math.round(this._selectedHour * (360 / (24 / 2)));
}
else {
deg = Math.round(this._selectedMinute * (360 / 60));
}
return {
height: `${radius}%`,
marginTop: `${50 - radius}%`,
transform: `rotate(${deg}deg)`,
};
}
ngAfterContentInit() {
this.activeDate = this._activeDate || this._adapter.today();
this._init();
}
ngOnDestroy() {
this._removeGlobalEvents();
}
ngOnChanges() {
this._init();
}
/** Binds our global move and end events. */
_bindGlobalEvents(triggerEvent) {
// Note that we bind the events to the `document`, because it allows us to capture
// drag cancel events where the user's pointer is outside the browser window.
const document = this._document;
const isTouch = isTouchEvent(triggerEvent);
const moveEventName = isTouch ? 'touchmove' : 'mousemove';
const endEventName = isTouch ? 'touchend' : 'mouseup';
document.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
document.addEventListener(endEventName, this._pointerUp, activeEventOptions);
if (isTouch) {
document.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
}
}
/** Removes any global event listeners that we may have added. */
_removeGlobalEvents() {
const document = this._document;
document.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
document.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
document.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
document.removeEventListener('touchend', this._pointerUp, activeEventOptions);
document.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
}
/** Initializes this clock view. */
_init() {
this._hours.length = 0;
this._minutes.length = 0;
const hourNames = this._adapter.getHourNames();
const minuteNames = this._adapter.getMinuteNames();
if (this.twelvehour) {
const hours = [];
for (let i = 0; i < hourNames.length; i++) {
const radian = (i / 6) * Math.PI;
const radius = CLOCK_OUTER_RADIUS;
const hour = i;
const date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), hour, 0);
// Check if the date is enabled, no need to respect the minute setting here
const enabled = (!this.minDate ||
this._adapter.compareDatetime(date, this.minDate, false) >= 0) &&
(!this.maxDate ||
this._adapter.compareDatetime(date, this.maxDate, false) <= 0) &&
(!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR));
// display value for twelvehour clock should be from 1-12 not including 0 and not above 12
hours.push({
value: i,
displayValue: i % 12 === 0 ? '12' : hourNames[i % 12],
enabled,
top: CLOCK_RADIUS - Math.cos(radian) * radius - CLOCK_TICK_RADIUS,
left: CLOCK_RADIUS + Math.sin(radian) * radius - CLOCK_TICK_RADIUS,
});
}
// filter out AM or PM hours based on AMPM
if (this.AMPM === 'AM') {
this._hours = hours.filter(x => x.value < 12);
}
else {
this._hours = hours.filter(x => x.value >= 12);
}
}
else {
for (let i = 0; i < hourNames.length; i++) {
const radian = (i / 6) * Math.PI;
const outer = i > 0 && i < 13;
const radius = outer ? CLOCK_OUTER_RADIUS : CLOCK_INNER_RADIUS;
const date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), i, 0);
// Check if the date is enabled, no need to respect the minute setting here
const enabled = (!this.minDate ||
this._adapter.compareDatetime(date, this.minDate, false) >= 0) &&
(!this.maxDate ||
this._adapter.compareDatetime(date, this.maxDate, false) <= 0) &&
(!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR));
this._hours.push({
value: i,
displayValue: i === 0 ? '00' : hourNames[i],
enabled,
top: CLOCK_RADIUS - Math.cos(radian) * radius - CLOCK_TICK_RADIUS,
left: CLOCK_RADIUS + Math.sin(radian) * radius - CLOCK_TICK_RADIUS,
fontSize: i > 0 && i < 13 ? '' : '80%',
});
}
}
for (let i = 0; i < minuteNames.length; i += 5) {
const radian = (i / 30) * Math.PI;
const date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), i);
const enabled = (!this.minDate || this._adapter.compareDatetime(date, this.minDate) >= 0) &&
(!this.maxDate || this._adapter.compareDatetime(date, this.maxDate) <= 0) &&
(!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.MINUTE));
this._minutes.push({
value: i,
displayValue: i === 0 ? '00' : minuteNames[i],
enabled,
top: CLOCK_RADIUS - Math.cos(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS,
left: CLOCK_RADIUS + Math.sin(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS,
});
}
}
/**
* Set Time
* @param event
*/
setTime(event) {
const trigger = this._elementRef.nativeElement;
const triggerRect = trigger.getBoundingClientRect();
const width = trigger.offsetWidth;
const height = trigger.offsetHeight;
const { pageX, pageY } = getPointerPositionOnPage(event);
const x = width / 2 - (pageX - triggerRect.left - window.pageXOffset);
const y = height / 2 - (pageY - triggerRect.top - window.pageYOffset);
let radian = Math.atan2(-x, y);
const unit = Math.PI / (this._hourView ? 6 : this.interval ? 30 / this.interval : 30);
const z = Math.sqrt(x * x + y * y);
const outer = this._hourView &&
z > (width * (CLOCK_OUTER_RADIUS / 100) + width * (CLOCK_INNER_RADIUS / 100)) / 2;
if (radian < 0) {
radian = Math.PI * 2 + radian;
}
let value = Math.round(radian / unit);
let date;
if (this._hourView) {
if (this.twelvehour) {
if (this.AMPM === 'AM') {
value = value === 0 ? 12 : value;
}
else {
// if we chosen 12 in PM, the value should be 0 for 0:00,
// else we can safely add 12 to the final value
value = value === 12 ? 0 : value + 12;
}
}
else {
if (value === 12) {
value = 0;
}
value = outer ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12;
}
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), value, this._adapter.getMinute(this.activeDate));
}
else {
if (this.interval) {
value *= this.interval;
}
if (value === 60) {
value = 0;
}
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
// if there is a dateFilter, check if the date is allowed if it is not then do not set/emit new date
// https://github.com/ng-matero/extensions/issues/244
if (this.dateFilter &&
!this.dateFilter(date, this._hourView ? MtxDatetimepickerFilterType.HOUR : MtxDatetimepickerFilterType.MINUTE)) {
return;
}
this._timeChanged = true;
this.activeDate = date;
this._changeDetectorRef.markForCheck();
this.activeDateChange.emit(this.activeDate);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxClock, deps: [{ token: i0.ElementRef }, { token: i1.DatetimeAdapter }, { token: i0.ChangeDetectorRef }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.0", type: MtxClock, isStandalone: true, selector: "mtx-clock", inputs: { dateFilter: "dateFilter", interval: "interval", twelvehour: ["twelvehour", "twelvehour", booleanAttribute], AMPM: "AMPM", activeDate: "activeDate", selected: "selected", minDate: "minDate", maxDate: "maxDate", startView: "startView" }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", _userSelection: "_userSelection" }, host: { attributes: { "role": "clock" }, listeners: { "mousedown": "_pointerDown($event)", "touchstart": "_pointerDown($event)" }, classAttribute: "mtx-clock" }, exportAs: ["mtxClock"], usesOnChanges: true, ngImport: i0, template: "<div class=\"mtx-clock-wrapper\">\n <div class=\"mtx-clock-center\"></div>\n <div class=\"mtx-clock-hand\" [style]=\"_hand\"></div>\n <div class=\"mtx-clock-hours\" [class.active]=\"_hourView\">\n @for (item of _hours; track item.value) {\n <div\n class=\"mtx-clock-cell\"\n [class.mtx-clock-cell-disabled]=\"!item.enabled\"\n [class.mtx-clock-cell-selected]=\"_selectedHour === item.value\"\n [style.fontSize]=\"item.fontSize\"\n [style.left]=\"item.left+'%'\"\n [style.top]=\"item.top+'%'\">{{ item.displayValue }}</div>\n }\n </div>\n <div class=\"mtx-clock-minutes\" [class.active]=\"!_hourView\">\n @for (item of _minutes; track item.value) {\n <div\n class=\"mtx-clock-cell\"\n [class.mtx-clock-cell-disabled]=\"!item.enabled\"\n [class.mtx-clock-cell-selected]=\"_selectedMinute === item.value\"\n [style.left]=\"item.left+'%'\"\n [style.top]=\"item.top+'%'\">{{ item.displayValue }}</div>\n }\n </div>\n</div>\n", styles: [".mtx-clock{position:relative;display:block;min-width:224px;margin:12px;box-sizing:border-box;-webkit-user-select:none;user-select:none;touch-action:none;font-size:var(--mtx-datetimepicker-clock-text-size, var(--mat-app-title-small-size))}.mtx-clock-wrapper{position:relative;width:100%;height:0;padding-top:100%;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-dial-background-color, var(--mat-app-surface-container-highest))}.mtx-clock-center{position:absolute;top:50%;left:50%;width:3%;height:3%;margin:-1.5%;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hand{position:absolute;inset:0;width:2px;margin:0 auto;transform-origin:bottom;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hand:before{content:\"\";position:absolute;top:-4px;left:-3px;width:8px;height:8px;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hours,.mtx-clock-minutes{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;visibility:hidden;transition:.35s;transform:scale(1.2)}.mtx-clock-hours.active,.mtx-clock-minutes.active{opacity:1;visibility:visible;transform:scale(1)}.mtx-clock-minutes{transform:scale(.8)}.mtx-clock-cell{position:absolute;display:flex;width:14.1666%;height:14.1666%;justify-content:center;box-sizing:border-box;border-radius:50%;align-items:center;cursor:pointer;color:var(--mtx-datetimepicker-clock-cell-text-color, var(--mat-app-on-surface))}.mtx-clock-cell.mtx-clock-cell-selected{color:#fff;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-cell:not(.mtx-clock-cell-selected,.mtx-clock-cell-disabled):hover{background-color:var(--mtx-datetimepicker-clock-cell-hover-state-background-color)}.mtx-clock-cell.mtx-clock-cell-disabled{pointer-events:none;color:var(--mtx-datetimepicker-clock-cell-disabled-state-text-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxClock, decorators: [{
type: Component,
args: [{ selector: 'mtx-clock', host: {
'role': 'clock',
'class': 'mtx-clock',
'(mousedown)': '_pointerDown($event)',
'(touchstart)': '_pointerDown($event)',
}, exportAs: 'mtxClock', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<div class=\"mtx-clock-wrapper\">\n <div class=\"mtx-clock-center\"></div>\n <div class=\"mtx-clock-hand\" [style]=\"_hand\"></div>\n <div class=\"mtx-clock-hours\" [class.active]=\"_hourView\">\n @for (item of _hours; track item.value) {\n <div\n class=\"mtx-clock-cell\"\n [class.mtx-clock-cell-disabled]=\"!item.enabled\"\n [class.mtx-clock-cell-selected]=\"_selectedHour === item.value\"\n [style.fontSize]=\"item.fontSize\"\n [style.left]=\"item.left+'%'\"\n [style.top]=\"item.top+'%'\">{{ item.displayValue }}</div>\n }\n </div>\n <div class=\"mtx-clock-minutes\" [class.active]=\"!_hourView\">\n @for (item of _minutes; track item.value) {\n <div\n class=\"mtx-clock-cell\"\n [class.mtx-clock-cell-disabled]=\"!item.enabled\"\n [class.mtx-clock-cell-selected]=\"_selectedMinute === item.value\"\n [style.left]=\"item.left+'%'\"\n [style.top]=\"item.top+'%'\">{{ item.displayValue }}</div>\n }\n </div>\n</div>\n", styles: [".mtx-clock{position:relative;display:block;min-width:224px;margin:12px;box-sizing:border-box;-webkit-user-select:none;user-select:none;touch-action:none;font-size:var(--mtx-datetimepicker-clock-text-size, var(--mat-app-title-small-size))}.mtx-clock-wrapper{position:relative;width:100%;height:0;padding-top:100%;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-dial-background-color, var(--mat-app-surface-container-highest))}.mtx-clock-center{position:absolute;top:50%;left:50%;width:3%;height:3%;margin:-1.5%;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hand{position:absolute;inset:0;width:2px;margin:0 auto;transform-origin:bottom;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hand:before{content:\"\";position:absolute;top:-4px;left:-3px;width:8px;height:8px;border-radius:50%;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-hours,.mtx-clock-minutes{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;visibility:hidden;transition:.35s;transform:scale(1.2)}.mtx-clock-hours.active,.mtx-clock-minutes.active{opacity:1;visibility:visible;transform:scale(1)}.mtx-clock-minutes{transform:scale(.8)}.mtx-clock-cell{position:absolute;display:flex;width:14.1666%;height:14.1666%;justify-content:center;box-sizing:border-box;border-radius:50%;align-items:center;cursor:pointer;color:var(--mtx-datetimepicker-clock-cell-text-color, var(--mat-app-on-surface))}.mtx-clock-cell.mtx-clock-cell-selected{color:#fff;background-color:var(--mtx-datetimepicker-clock-hand-background-color, var(--mat-app-primary))}.mtx-clock-cell:not(.mtx-clock-cell-selected,.mtx-clock-cell-disabled):hover{background-color:var(--mtx-datetimepicker-clock-cell-hover-state-background-color)}.mtx-clock-cell.mtx-clock-cell-disabled{pointer-events:none;color:var(--mtx-datetimepicker-clock-cell-disabled-state-text-color)}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1.DatetimeAdapter }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }], propDecorators: { dateFilter: [{
type: Input
}], interval: [{
type: Input
}], twelvehour: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], AMPM: [{
type: Input
}], selectedChange: [{
type: Output
}], activeDateChange: [{
type: Output
}], _userSelection: [{
type: Output
}], activeDate: [{
type: Input
}], selected: [{
type: Input
}], minDate: [{
type: Input
}], maxDate: [{
type: Input
}], startView: [{
type: Input
}] } });
/** Returns whether an event is a touch event. */
function isTouchEvent(event) {
// This function is called for every pixel that the user has dragged so we need it to be
// as fast as possible. Since we only bind mouse events and touch events, we can assume
// that if the event's name starts with `t`, it's a touch event.
return event.type[0] === 't';
}
/** Gets the coordinates of a touch or mouse event relative to the document. */
function getPointerPositionOnPage(event) {
let point;
if (isTouchEvent(event)) {
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
point = event.touches[0] || event.changedTouches[0];
}
else {
point = event;
}
return point;
}
/**
* Animations used by the Material datetimepicker.
* @docs-private
*/
const mtxDatetimepickerAnimations = {
/** Transforms the height of the datetimepicker's calendar. */
transformPanel: trigger('transformPanel', [
transition('void => enter-dropdown', animate('120ms cubic-bezier(0, 0, 0.2, 1)', keyframes([
style({ opacity: 0, transform: 'scale(1, 0.8)' }),
style({ opacity: 1, transform: 'scale(1, 1)' }),
]))),
transition('void => enter-dialog', animate('150ms cubic-bezier(0, 0, 0.2, 1)', keyframes([
style({ opacity: 0, transform: 'scale(0.7)' }),
style({ transform: 'none', opacity: 1 }),
]))),
transition('* => void', animate('100ms linear', style({ opacity: 0 }))),
]),
/** Fades in the content of the calendar. */
fadeInCalendar: trigger('fadeInCalendar', [
state('void', style({ opacity: 0 })),
state('enter', style({ opacity: 1 })),
// TODO(crisbeto): this animation should be removed since it isn't quite on spec, but we
// need to keep it until #12440 gets in, otherwise the exit animation will look glitchy.
transition('void => *', animate('120ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
]),
slideCalendar: trigger('slideCalendar', [
transition('* => left', [
animate(180, keyframes([
style({ transform: 'translateX(100%)', offset: 0.5 }),
style({ transform: 'translateX(-100%)', offset: 0.51 }),
style({ transform: 'translateX(0)', offset: 1 }),
])),
]),
transition('* => right', [
animate(180, keyframes([
style({ transform: 'translateX(-100%)', offset: 0.5 }),
style({ transform: 'translateX(100%)', offset: 0.51 }),
style({ transform: 'translateX(0)', offset: 1 }),
])),
]),
]),
};
/** @docs-private */
function createMissingDateImplError(provider) {
return Error(`MtxDatetimepicker: No provider found for ${provider}. You must add one of the following ` +
`to your app config: provideNativeDatetimeAdapter, provideDateFnsDatetimeAdapter,` +
`provideLuxonDatetimeAdapter, provideMomentDatetimeAdapter, or provide a ` +
`custom implementation.`);
}
/**
* An internal class that represents the data corresponding to a single calendar cell.
* @docs-private
*/
class MtxCalendarCell {
constructor(value, displayValue, ariaLabel, enabled) {
this.value = value;
this.displayValue = displayValue;
this.ariaLabel = ariaLabel;
this.enabled = enabled;
}
}
/**
* An internal component used to display calendar data in a table.
* @docs-private
*/
class MtxCalendarBody {
constructor() {
/** The number of columns in the table. */
this.numCols = 7;
/** Whether to allow selection of disabled cells. */
this.allowDisabledSelection = false;
/** The cell number of the active cell in the table. */
this.activeCell = 0;
/** Emits when a new value is selected. */
this.selectedValueChange = new EventEmitter();
}
/** The number of blank cells to put at the beginning for the first row. */
get _firstRowOffset() {
return this.rows && this.rows.length && this.rows[0].length
? this.numCols - this.rows[0].length
: 0;
}
_cellClicked(cell) {
if (!this.allowDisabledSelection && !cell.enabled) {
return;
}
this.selectedValueChange.emit(cell.value);
}
_isActiveCell(rowIndex, colIndex) {
let cellNumber = rowIndex * this.numCols + colIndex;
// Account for the fact that the first row may not have as many cells.
if (rowIndex) {
cellNumber -= this._firstRowOffset;
}
return cellNumber === this.activeCell;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxCalendarBody, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.0", type: MtxCalendarBody, isStandalone: true, selector: "[mtx-calendar-body]", inputs: { label: "label", rows: "rows", todayValue: "todayValue", selectedValue: "selectedValue", labelMinRequiredCells: "labelMinRequiredCells", numCols: "numCols", allowDisabledSelection: "allowDisabledSelection", activeCell: "activeCell" }, outputs: { selectedValueChange: "selectedValueChange" }, host: { classAttribute: "mtx-calendar-body" }, exportAs: ["mtxCalendarBody"], ngImport: i0, template: "<!--\n If there's not enough space in the first row, create a separate label row. We mark this row as\n aria-hidden because we don't want it to be read out as one of the weeks in the month.\n-->\n@if (_firstRowOffset < labelMinRequiredCells) {\n <tr aria-hidden=\"true\">\n <td class=\"mtx-calendar-body-label\" [attr.colspan]=\"numCols\">{{ label }}</td>\n </tr>\n}\n\n<!-- Create the first row separately so we can include a special spacer cell. -->\n@for (row of rows; track row; let rowIndex = $index) {\n <tr role=\"row\">\n <!--\n We mark this cell as aria-hidden so it doesn't get read out as one of the days in the week.\n -->\n @if (rowIndex === 0 && _firstRowOffset) {\n <td\n class=\"mtx-calendar-body-label\" [attr.colspan]=\"_firstRowOffset\" aria-hidden=\"true\">\n {{ _firstRowOffset >= labelMinRequiredCells ? label : '' }}\n </td>\n }\n @for (item of row; track item; let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"mtx-calendar-body-cell\"\n [class.mtx-calendar-body-active]=\"_isActiveCell(rowIndex, colIndex)\"\n [class.mtx-calendar-body-disabled]=\"!item.enabled\"\n [tabindex]=\"_isActiveCell(rowIndex, colIndex) ? 0 : -1\"\n [attr.data-mat-row]=\"rowIndex\"\n [attr.data-mat-col]=\"colIndex\"\n [attr.aria-label]=\"item.ariaLabel\"\n [attr.aria-disabled]=\"!item.enabled || null\"\n (click)=\"_cellClicked(item)\">\n <div class=\"mtx-calendar-body-cell-content\"\n [class.mtx-calendar-body-selected]=\"selectedValue === item.value\"\n [class.mtx-calendar-body-today]=\"todayValue === item.value\"\n [attr.aria-selected]=\"selectedValue === item.value\">\n {{ item.displayValue }}\n </div>\n </td>\n }\n </tr>\n}\n", styles: [".mtx-calendar-body{min-width:224px}.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-today-outline-color, var(--mat-app-primary))}.mtx-calendar-body-label{height:0;line-height:0;text-align:left;padding:7.1428571429% 4.7142857143%;font-size:var(--mtx-datetimepicker-calendar-body-label-text-size, var(--mat-app-title-small-size));font-weight:var(--mtx-datetimepicker-calendar-body-label-text-weight, var(--mat-app-title-small-weight));color:var(--mtx-datetimepicker-calendar-body-label-text-color, var(--mat-app-on-surface))}[dir=rtl] .mtx-calendar-body-label{text-align:right}.mtx-calendar-body-cell{position:relative;width:14.2857142857%;height:0;line-height:0;padding:7.1428571429% 0;text-align:center;outline:none;cursor:pointer}.mtx-calendar-body-disabled{cursor:default;pointer-events:none}.mtx-calendar-body-disabled>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color)}.mtx-calendar-body-disabled>.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color)}.mtx-calendar-body-cell-content{position:absolute;top:5%;left:5%;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;border:1px solid transparent;border-radius:999px;color:var(--mtx-datetimepicker-calendar-date-text-color, var(--mat-app-on-surface));border-color:var(--mtx-datetimepicker-calendar-date-outline-color)}.mtx-calendar-body-active>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){background-color:var(--mtx-datetimepicker-calendar-date-focus-state-background-color)}@media (hover: hover){.mtx-calendar-body-cell:not(.mtx-calendar-body-disabled):hover>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){background-color:var(--mtx-datetimepicker-calendar-date-hover-state-background-color)}}.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-state-background-color, var(--mat-app-primary));color:var(--mtx-datetimepicker-calendar-date-selected-state-text-color, var(--mat-app-on-primary))}.mtx-calendar-body-disabled>.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-disabled-state-background-color)}.mtx-calendar-body-selected.mtx-calendar-body-today{box-shadow:inset 0 0 0 1px var(--mtx-datetimepicker-calendar-date-today-selected-state-outline-color, var(--mat-app-primary))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxCalendarBody, decorators: [{
type: Component,
args: [{ selector: '[mtx-calendar-body]', host: {
class: 'mtx-calendar-body',
}, exportAs: 'mtxCalendarBody', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<!--\n If there's not enough space in the first row, create a separate label row. We mark this row as\n aria-hidden because we don't want it to be read out as one of the weeks in the month.\n-->\n@if (_firstRowOffset < labelMinRequiredCells) {\n <tr aria-hidden=\"true\">\n <td class=\"mtx-calendar-body-label\" [attr.colspan]=\"numCols\">{{ label }}</td>\n </tr>\n}\n\n<!-- Create the first row separately so we can include a special spacer cell. -->\n@for (row of rows; track row; let rowIndex = $index) {\n <tr role=\"row\">\n <!--\n We mark this cell as aria-hidden so it doesn't get read out as one of the days in the week.\n -->\n @if (rowIndex === 0 && _firstRowOffset) {\n <td\n class=\"mtx-calendar-body-label\" [attr.colspan]=\"_firstRowOffset\" aria-hidden=\"true\">\n {{ _firstRowOffset >= labelMinRequiredCells ? label : '' }}\n </td>\n }\n @for (item of row; track item; let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"mtx-calendar-body-cell\"\n [class.mtx-calendar-body-active]=\"_isActiveCell(rowIndex, colIndex)\"\n [class.mtx-calendar-body-disabled]=\"!item.enabled\"\n [tabindex]=\"_isActiveCell(rowIndex, colIndex) ? 0 : -1\"\n [attr.data-mat-row]=\"rowIndex\"\n [attr.data-mat-col]=\"colIndex\"\n [attr.aria-label]=\"item.ariaLabel\"\n [attr.aria-disabled]=\"!item.enabled || null\"\n (click)=\"_cellClicked(item)\">\n <div class=\"mtx-calendar-body-cell-content\"\n [class.mtx-calendar-body-selected]=\"selectedValue === item.value\"\n [class.mtx-calendar-body-today]=\"todayValue === item.value\"\n [attr.aria-selected]=\"selectedValue === item.value\">\n {{ item.displayValue }}\n </div>\n </td>\n }\n </tr>\n}\n", styles: [".mtx-calendar-body{min-width:224px}.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-today-outline-color, var(--mat-app-primary))}.mtx-calendar-body-label{height:0;line-height:0;text-align:left;padding:7.1428571429% 4.7142857143%;font-size:var(--mtx-datetimepicker-calendar-body-label-text-size, var(--mat-app-title-small-size));font-weight:var(--mtx-datetimepicker-calendar-body-label-text-weight, var(--mat-app-title-small-weight));color:var(--mtx-datetimepicker-calendar-body-label-text-color, var(--mat-app-on-surface))}[dir=rtl] .mtx-calendar-body-label{text-align:right}.mtx-calendar-body-cell{position:relative;width:14.2857142857%;height:0;line-height:0;padding:7.1428571429% 0;text-align:center;outline:none;cursor:pointer}.mtx-calendar-body-disabled{cursor:default;pointer-events:none}.mtx-calendar-body-disabled>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color)}.mtx-calendar-body-disabled>.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color)}.mtx-calendar-body-cell-content{position:absolute;top:5%;left:5%;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;border:1px solid transparent;border-radius:999px;color:var(--mtx-datetimepicker-calendar-date-text-color, var(--mat-app-on-surface));border-color:var(--mtx-datetimepicker-calendar-date-outline-color)}.mtx-calendar-body-active>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){background-color:var(--mtx-datetimepicker-calendar-date-focus-state-background-color)}@media (hover: hover){.mtx-calendar-body-cell:not(.mtx-calendar-body-disabled):hover>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected){background-color:var(--mtx-datetimepicker-calendar-date-hover-state-background-color)}}.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-state-background-color, var(--mat-app-primary));color:var(--mtx-datetimepicker-calendar-date-selected-state-text-color, var(--mat-app-on-primary))}.mtx-calendar-body-disabled>.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-disabled-state-background-color)}.mtx-calendar-body-selected.mtx-calendar-body-today{box-shadow:inset 0 0 0 1px var(--mtx-datetimepicker-calendar-date-today-selected-state-outline-color, var(--mat-app-primary))}\n"] }]
}], propDecorators: { label: [{
type: Input
}], rows: [{
type: Input
}], todayValue: [{
type: Input
}], selectedValue: [{
type: Input
}], labelMinRequiredCells: [{
type: Input
}], numCols: [{
type: Input
}], allowDisabledSelection: [{
type: Input
}], activeCell: [{
type: Input
}], selectedValueChange: [{
type: Output
}] } });
const DAYS_PER_WEEK = 7;
/**
* An internal component used to display a single month in the datetimepicker.
* @docs-private
*/
class MtxMonthView {
constructor(_adapter, _dateFormats) {
this._adapter = _adapter;
this._dateFormats = _dateFormats;
this.type = 'date';
/** Emits when a new date is selected. */
this.selectedChange = new EventEmitter();
/** Emits when any date is selected. */
this._userSelection = new EventEmitter();
if (!this._adapter) {
throw createMissingDateImplError('DatetimeAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MTX_DATETIME_FORMATS');
}
const firstDayOfWeek = this._adapter.getFirstDayOfWeek();
const narrowWeekdays = this._adapter.getDayOfWeekNames('narrow');
const longWeekdays = this._adapter.getDayOfWeekNames('long');
// Rotate the labels for days of the week based on the configured first day of the week.
const weekdays = longWeekdays.map((long, i) => {
return { long, narrow: narrowWeekdays[i] };
});
this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
this._activeDate = this._adapter.today();
}
/**
* The date to display in this month view (everything other than the month and year is ignored).
*/
get activeDate() {
return this._activeDate;
}
set activeDate(value) {
const oldActiveDate = this._activeDate;
this._activeDate = value || this._adapter.today();
if (oldActiveDate &&
this._activeDate &&
!this._adapter.sameMonthAndYear(oldActiveDate, this._activeDate)) {
this._init();
if (this._adapter.isInNextMonth(oldActiveDate, this._activeDate)) {
this.calendarState('right');
}
else {
this.calendarState('left');
}
}
}
/** The currently selected date. */
get selected() {
return this._selected;
}
set selected(value) {
this._selected = value;
this._selectedDate = this._getDateInCurrentMonth(this.selected);
}
ngAfterContentInit() {
this._init();
}
/** Handles when a new date is selected. */
_dateSelected(date) {
const dateObject = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), date, this._adapter.getHour(this.activeDate), this._adapter.getMinute(this.activeDate));
this.selectedChange.emit(dateObject);
this._activeDate = dateObject;
if (this.type === 'date') {
this._userSelection.emit();
}
}
_calendarStateDone() {
this._calendarState = '';
}
/** Initializes this month view. */
_init() {
this._selectedDate = this._getDateInCurrentMonth(this.selected);
this._todayDate = this._getDateInCurrentMonth(this._adapter.today());
const firstOfMonth = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), 1, this._adapter.getHour(this.activeDate), this._adapter.getMinute(this.activeDate));
this._firstWeekOffset =
(DAYS_PER_WEEK +
this._adapter.getDayOfWeek(firstOfMonth) -
this._adapter.getFirstDayOfWeek()) %
DAYS_PER_WEEK;
this._createWeekCells();
}
/** Creates MdCalendarCells for the dates in this month. */
_createWeekCells() {
const daysInMonth = this._adapter.getNumDaysInMonth(this.activeDate);
const dateNames = this._adapter.getDateNames();
this._weeks = [[]];
for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {
if (cell === DAYS_PER_WEEK) {
this._weeks.push([]);
cell = 0;
}
const date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), i + 1, this._adapter.getHour(this.activeDate), this._adapter.getMinute(this.activeDate));
const enabled = !this.dateFilter || this.dateFilter(date);
const ariaLabel = this._adapter.format(date, this._dateFormats.display.dateA11yLabel);
this._weeks[this._weeks.length - 1].push(new MtxCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
}
}
/**
* Gets the date in this month that the given Date falls on.
* Returns null if the given Date is in another month.
*/
_getDateInCurrentMonth(date) {
return this._adapter.sameMonthAndYear(date, this.activeDate)
? this._adapter.getDate(date)
: null;
}
calendarState(direction) {
this._calendarState = direction;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxMonthView, deps: [{ token: i1.DatetimeAdapter, optional: true }, { token: MTX_DATETIME_FORMATS, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.0", type: MtxMonthView, isStandalone: true, selector: "mtx-month-view", inputs: { type: "type", dateFilter: "dateFilter", activeDate: "activeDate", selected: "selected" }, outputs: { selectedChange: "selectedChange", _userSelection: "_userSelection" }, exportAs: ["mtxMonthView"], ngImport: i0, template: "<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\">\n <tr>\n @for (day of _weekdays; track day) {\n <th [attr.aria-label]=\"day.long\">{{day.narrow}}</th>\n }\n </tr>\n </thead>\n <tbody mtx-calendar-body\n (@slideCalendar.done)=\"_calendarStateDone()\"\n [@slideCalendar]=\"_calendarState\"\n [rows]=\"_weeks\"\n [todayValue]=\"_todayDate!\"\n [activeCell]=\"_adapter.getDate(activeDate) - 1\"\n [selectedValue]=\"_selectedDate!\"\n (selectedValueChange)=\"_dateSelected($event)\"></tbody>\n</table>\n", dependencies: [{ kind: "component", type: MtxCalendarBody, selector: "[mtx-calendar-body]", inputs: ["label", "rows", "todayValue", "selectedValue", "labelMinRequiredCells", "numCols", "allowDisabledSelection", "activeCell"], outputs: ["selectedValueChange"], exportAs: ["mtxCalendarBody"] }], animations: [mtxDatetimepickerAnimations.slideCalendar], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.0", ngImport: i0, type: MtxMonthView, decorators: [{
type: Component,
args: [{ selector: 'mtx-month-view', exportAs: 'mtxMonthView', animations: [mtxDatetimepickerAnimations.slideCalendar], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MtxCalendarBody], template: "<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\">\n <tr>\n @for (day of _weekdays; track day) {\n <th [attr.aria-label]=\"day.long\">{{day.narrow}}</th>\n }\n </tr>\n </thead>\n <tbody mtx-calendar-body\n (@slideCalendar.done)=\"_calendarStateDone()\"\n [@slideCalendar]=\"_calendarState\"\n [rows]=\"_weeks\"\n [todayValue]=\"_todayDate!\"\n [activeCell]=\"_adapter.getDate(activeDate) - 1\"\n [selectedValue]=\"_selectedDate!\"\n (selectedValueChange)=\"_dateSelected($event)\"></tbody>\n</table>\n" }]
}], ctorParameters: () => [{ type: i1.DatetimeAdapter, decorators: [{
type: Optional
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [MTX_DATETIME_FORMATS]
}] }], propDecorators: { type: [{
type: Input
}], dateFilter: [{
type: Input
}], selectedChange: [{
type: Output
}], _userSelection: [{
type: Output
}], activeDate: [{
type: Input
}], selected: [{
type: Input
}] } });
const yearsPerPage = 24;
const yearsPerRow = 4;
/**
* An internal component used to display multiple years in the datetimepicker.
* @docs-private
*/
class MtxMultiYearView {
constructor(_adapter, _dateFormats) {
this._adapter = _adapter;
this._dateFormats = _dateFormats;
this.type = 'date';
/** Emits when a new month is selected. */
this.selectedChange = new EventEmitter();
/** Emits when any date is selected. */
this._userSelection = new EventEmitter();
if (!this._adapter) {
throw createMissingDateImplError('DatetimeAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MTX_DATETIME_FORMATS');
}
this._activeDate = this._adapter.today();
}
/** The date to display in this multi year view */
get activeDate() {
return this._activeDate;
}
set activeDate(value) {
const oldActiveDate = this._activeDate;
this._activeDate = value || this._adapter.today();
if (oldActiveDate &&
this._activeDate &&
!isSameMultiYearView(this._adapter, oldActiveDate, this._activeDate, this.minDate, this.maxDate)) {
this._init();
}
}
/** The currently selected date. */
get selected() {
return this._selected;
}
set selected(value) {
this._selected = value;
this._selectedYear = this._selected && this._adapter.getYear(this._selected);
}
/** The minimum selectable date. */
get minDate() {
return this._minDate;
}
set minDate(value) {
this._minDate = this._getValidDateOrNull(this._adapter.deserialize(value));
}
/** The maximum selectable date. */
get maxDate() {
return this._maxDate;
}
set maxDate(value) {
this._maxDate = this._getValidDateOrNull(this._adapter.deserialize(value));
}
ngAfterContentInit() {
this._init();
}
/** Handles when a new year is selected. */
_yearSelected(year) {
const month = this._adapter.getMonth(this.activeDate);
const normalizedDate = this._adapter.createDatetime(year, month, 1, 0, 0);
const da