@ng-matero/extensions
Version:
Angular Material Extensions
642 lines (635 loc) • 254 kB
JavaScript
import { CdkMonitorFocus, CdkTrapFocus, A11yModule } from '@angular/cdk/a11y';
import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
import { ComponentPortal, CdkPortalOutlet, TemplatePortal, PortalModule } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, inject, ElementRef, NgZone, Injector, EventEmitter, afterNextRender, booleanAttribute, Output, Input, ChangeDetectionStrategy, ViewEncapsulation, Component, ViewChild, ChangeDetectorRef, DOCUMENT, Directive, InjectionToken, Renderer2, ViewContainerRef, numberAttribute, forwardRef, HostAttributeToken, ContentChild, TemplateRef, NgModule } from '@angular/core';
import { MatButton, MatIconButton, MatButtonModule } from '@angular/material/button';
import { DatetimeAdapter, MTX_DATETIME_FORMATS } from '@ng-matero/extensions/core';
import { Subject, Subscription, merge, of } from 'rxjs';
import { Directionality } from '@angular/cdk/bidi';
import { SPACE, ENTER, PAGE_DOWN, PAGE_UP, END, HOME, DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW, hasModifierKey, ESCAPE } from '@angular/cdk/keycodes';
import { coerceNumberProperty, coerceStringArray } from '@angular/cdk/coercion';
import { normalizePassiveListenerOptions, _getFocusedElementPierceShadowDom } from '@angular/cdk/platform';
import { _animationsDisabled } from '@angular/material/core';
import { take, filter } from 'rxjs/operators';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, Validators } from '@angular/forms';
import { MatFormField } from '@angular/material/form-field';
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';
/** @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.`);
}
var MtxDatetimepickerFilterType;
(function (MtxDatetimepickerFilterType) {
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["DATE"] = 0] = "DATE";
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["HOUR"] = 1] = "HOUR";
MtxDatetimepickerFilterType[MtxDatetimepickerFilterType["MINUTE"] = 2] = "MINUTE";
})(MtxDatetimepickerFilterType || (MtxDatetimepickerFilterType = {}));
class MtxDatetimepickerIntl {
constructor() {
/**
* Stream to emit from when labels are changed. Use this to notify components when the labels have
* changed after initialization.
*/
this.changes = new Subject();
/** A label for the calendar popup (used by screen readers). */
this.calendarLabel = 'Calendar';
/** A label for the button used to open the calendar popup (used by screen readers). */
this.openCalendarLabel = 'Open calendar';
/** Label for the button used to close the calendar popup. */
this.closeCalendarLabel = 'Close calendar';
/** A label for the previous month button (used by screen readers). */
this.prevMonthLabel = 'Previous month';
/** A label for the next month button (used by screen readers). */
this.nextMonthLabel = 'Next month';
/** A label for the previous year button (used by screen readers). */
this.prevYearLabel = 'Previous year';
/** A label for the next year button (used by screen readers). */
this.nextYearLabel = 'Next year';
/** A label for the previous multi-year button (used by screen readers). */
this.prevMultiYearLabel = 'Previous 24 years';
/** A label for the next multi-year button (used by screen readers). */
this.nextMultiYearLabel = 'Next 24 years';
/** A label for the 'switch to month view' button (used by screen readers). */
this.switchToMonthViewLabel = 'Choose date';
/** A label for the 'switch to year view' button (used by screen readers). */
this.switchToYearViewLabel = 'Choose month';
/** A label for the 'switch to multi-year view' button (used by screen readers). */
this.switchToMultiYearViewLabel = 'Choose month and year';
/** A label for the first date of a range of dates (used by screen readers). */
this.startDateLabel = 'Start date';
/** A label for the last date of a range of dates (used by screen readers). */
this.endDateLabel = 'End date';
/** A label for the 'switch to clock hour view' button (used by screen readers). */
this.switchToClockHourViewLabel = 'Choose hour';
/** A label for the 'switch to clock minute view' button (used by screen readers). */
this.switchToClockMinuteViewLabel = 'Choose minute';
/** Label used for ok button within the manual time input. */
this.okLabel = 'OK';
/** Label used for cancel button within the manual time input. */
this.cancelLabel = 'Cancel';
}
/** A label for the week numbers (used by screen readers). */
weekNumberLabel(number) {
return `Week ${number}`;
}
/** Formats a range of years (used for visuals). */
formatYearRange(start, end) {
return `${start} \u2013 ${end}`;
}
/** Formats a label for a range of years (used by screen readers). */
formatYearRangeLabel(start, end) {
return `${start} to ${end}`;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDatetimepickerIntl, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDatetimepickerIntl, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxDatetimepickerIntl, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
let uniqueIdCounter$1 = 0;
/**
* An internal class that represents the data corresponding to a single calendar cell.
* @docs-private
*/
class MtxCalendarCell {
constructor(value, displayValue, ariaLabel, enabled, isWeekNumber) {
this.value = value;
this.displayValue = displayValue;
this.ariaLabel = ariaLabel;
this.enabled = enabled;
this.isWeekNumber = isWeekNumber;
this.id = uniqueIdCounter$1++;
}
}
/**
* An internal component used to display calendar data in a table.
* @docs-private
*/
class MtxCalendarBody {
constructor() {
this._elementRef = inject(ElementRef);
this._ngZone = inject(NgZone);
this._injector = inject(Injector);
/** Whether to show week numbers */
this.showWeekNumbers = false;
/**
* The aspect ratio (width / height) to use for the cells in the table. This aspect ratio will be
* maintained even as the table resizes.
*/
this.cellAspectRatio = 1;
/** 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();
/**
* Tracking function for rows based on their identity. Ideally we would use some sort of
* key on the row, but that would require a breaking change for the `rows` input. We don't
* use the built-in identity tracking, because it logs warnings.
*/
this._trackRow = (row) => row;
}
_cellClicked(cell) {
if (!this.allowDisabledSelection && !cell.enabled) {
return;
}
this.selectedValueChange.emit(cell.value);
}
_emitActiveDateChange(cell, event) {
if (cell.enabled) {
// this.activeDateChange.emit({ value: cell.value, event });
}
}
_isActiveCell(rowIndex, colIndex) {
const week = this.rows[rowIndex];
const cell = week[colIndex];
if (cell.isWeekNumber) {
return false;
}
return cell.value === this.activeCell + 1;
}
ngOnChanges(changes) {
const columnChanges = changes['numCols'];
const { rows, numCols } = this;
if (changes['rows'] || columnChanges) {
this._firstRowOffset = rows && rows.length && rows[0].length ? numCols - rows[0].length : 0;
}
if (changes['cellAspectRatio'] || columnChanges || !this._cellPadding) {
this._cellPadding = `${(50 * this.cellAspectRatio) / numCols}%`;
}
if (columnChanges || !this._cellWidth) {
this._cellWidth = `${100 / numCols}%`;
}
}
_focusActiveCell(movePreview = true) {
afterNextRender(() => {
setTimeout(() => {
const activeCell = this._elementRef.nativeElement.querySelector('.mtx-calendar-body-active');
if (activeCell) {
activeCell.focus();
}
});
}, { injector: this._injector });
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxCalendarBody, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MtxCalendarBody, isStandalone: true, selector: "[mtx-calendar-body]", inputs: { label: "label", rows: "rows", showWeekNumbers: ["showWeekNumbers", "showWeekNumbers", booleanAttribute], cellAspectRatio: "cellAspectRatio", todayValue: "todayValue", selectedValue: "selectedValue", labelMinRequiredCells: "labelMinRequiredCells", numCols: "numCols", allowDisabledSelection: "allowDisabledSelection", activeCell: "activeCell" }, outputs: { selectedValueChange: "selectedValueChange" }, host: { attributes: { "role": "grid", "aria-readonly": "true" }, classAttribute: "mtx-calendar-body" }, exportAs: ["mtxCalendarBody"], usesOnChanges: true, 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\n class=\"mtx-calendar-body-label\"\n [attr.colspan]=\"numCols\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n >\n {{ label }}\n </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 _trackRow(row); let rowIndex = $index) {\n <tr role=\"row\">\n @if (row[0].isWeekNumber) {\n <th\n class=\"mtx-calendar-body-week-number\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n [attr.aria-label]=\"row[0].ariaLabel\"\n >\n {{ row[0].displayValue }}\n </th>\n }\n\n <!--\n This cell is purely decorative, but we can't put `aria-hidden` or `role=\"presentation\"` on it,\n because it throws off the week days for the rest of the row on NVDA. The aspect ratio of the\n table cells is maintained by setting the top and bottom padding as a percentage of the width\n (a variant of the trick described here: https://www.w3schools.com/howto/howto_css_aspect_ratio.asp).\n -->\n @if (rowIndex === 0 && _firstRowOffset) {\n <td\n class=\"mtx-calendar-body-label\"\n [attr.colspan]=\"_firstRowOffset\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n >\n {{ _firstRowOffset >= labelMinRequiredCells ? label : '' }}\n </td>\n }\n <!--\n Each gridcell in the calendar contains a button, which signals to assistive technology that the\n cell is interactable, as well as the selection state via `aria-pressed`. See #23476 for\n background.\n -->\n @for (item of row; track item.id; let colIndex = $index) {\n @if (!item.isWeekNumber) {\n <td\n role=\"gridcell\"\n class=\"mtx-calendar-body-cell-container\"\n [style.width]=\"_cellWidth\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n [attr.data-mat-row]=\"rowIndex\"\n [attr.data-mat-col]=\"colIndex\"\n >\n <button\n type=\"button\"\n class=\"mtx-calendar-body-cell\"\n [tabindex]=\"_isActiveCell(rowIndex, colIndex) ? 0 : -1\"\n [class.mtx-calendar-body-disabled]=\"!item.enabled\"\n [class.mtx-calendar-body-active]=\"_isActiveCell(rowIndex, colIndex)\"\n [attr.aria-label]=\"item.ariaLabel\"\n [attr.aria-disabled]=\"!item.enabled || null\"\n (click)=\"_cellClicked(item)\"\n >\n <span\n class=\"mtx-calendar-body-cell-content mat-focus-indicator\"\n [class.mtx-calendar-body-selected]=\"selectedValue === item.value\"\n [class.mtx-calendar-body-today]=\"todayValue === item.value\"\n >\n {{ item.displayValue }}\n </span>\n </button>\n </td>\n }\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-sys-primary))}.mtx-calendar-body-label{height:0;line-height:0;text-align:left;padding-left:4.7142857143%;padding-right:4.7142857143%;font-size:var(--mtx-datetimepicker-calendar-body-label-text-size, var(--mat-sys-title-small-size));font-weight:var(--mtx-datetimepicker-calendar-body-label-text-weight, var(--mat-sys-title-small-weight));color:var(--mtx-datetimepicker-calendar-body-label-text-color, var(--mat-sys-on-surface))}[dir=rtl] .mtx-calendar-body-label{text-align:right}.mtx-calendar-body-week-number{height:0;line-height:0;font-weight:400;color:var(--mtx-datetimepicker-calendar-body-week-number-text-color, var(--mat-sys-secondary))}.mtx-calendar-body-cell-container{position:relative;height:0;line-height:0}.mtx-calendar-body-cell{position:absolute;top:0;left:0;width:100%;height:100%;background:none;text-align:center;margin:0;font-family:var(--mtx-datetimepicker-calendar-text-font, var(--mat-sys-body-medium-font));font-size:var(--mtx-datetimepicker-calendar-text-size, var(--mat-sys-body-medium-size));-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent}.mtx-calendar-body-cell::-moz-focus-inner{border:0}.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, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mtx-calendar-body-disabled>.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors:active){.mtx-calendar-body-disabled{opacity:.5}}.mtx-calendar-body-cell-content{top:5%;left:5%;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;line-height:1;border:1px;border-style:solid;border-radius:999px;color:var(--mtx-datetimepicker-calendar-date-text-color, var(--mat-sys-on-surface));border-color:var(--mtx-datetimepicker-calendar-date-outline-color, transparent)}.mtx-calendar-body-cell-content.mat-focus-indicator{position:absolute}@media(forced-colors:active){.mtx-calendar-body-cell-content{border:none}}.cdk-keyboard-focused .mtx-calendar-body-active>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected),.cdk-program-focused .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, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@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, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}}.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-state-background-color, var(--mat-sys-primary));color:var(--mtx-datetimepicker-calendar-date-selected-state-text-color, var(--mat-sys-on-primary))}.mtx-calendar-body-disabled>.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-disabled-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.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-sys-primary))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxCalendarBody, decorators: [{
type: Component,
args: [{ selector: '[mtx-calendar-body]', host: {
'class': 'mtx-calendar-body',
'role': 'grid',
'aria-readonly': 'true',
}, exportAs: 'mtxCalendarBody', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, 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\n class=\"mtx-calendar-body-label\"\n [attr.colspan]=\"numCols\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n >\n {{ label }}\n </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 _trackRow(row); let rowIndex = $index) {\n <tr role=\"row\">\n @if (row[0].isWeekNumber) {\n <th\n class=\"mtx-calendar-body-week-number\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n [attr.aria-label]=\"row[0].ariaLabel\"\n >\n {{ row[0].displayValue }}\n </th>\n }\n\n <!--\n This cell is purely decorative, but we can't put `aria-hidden` or `role=\"presentation\"` on it,\n because it throws off the week days for the rest of the row on NVDA. The aspect ratio of the\n table cells is maintained by setting the top and bottom padding as a percentage of the width\n (a variant of the trick described here: https://www.w3schools.com/howto/howto_css_aspect_ratio.asp).\n -->\n @if (rowIndex === 0 && _firstRowOffset) {\n <td\n class=\"mtx-calendar-body-label\"\n [attr.colspan]=\"_firstRowOffset\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n >\n {{ _firstRowOffset >= labelMinRequiredCells ? label : '' }}\n </td>\n }\n <!--\n Each gridcell in the calendar contains a button, which signals to assistive technology that the\n cell is interactable, as well as the selection state via `aria-pressed`. See #23476 for\n background.\n -->\n @for (item of row; track item.id; let colIndex = $index) {\n @if (!item.isWeekNumber) {\n <td\n role=\"gridcell\"\n class=\"mtx-calendar-body-cell-container\"\n [style.width]=\"_cellWidth\"\n [style.paddingTop]=\"_cellPadding\"\n [style.paddingBottom]=\"_cellPadding\"\n [attr.data-mat-row]=\"rowIndex\"\n [attr.data-mat-col]=\"colIndex\"\n >\n <button\n type=\"button\"\n class=\"mtx-calendar-body-cell\"\n [tabindex]=\"_isActiveCell(rowIndex, colIndex) ? 0 : -1\"\n [class.mtx-calendar-body-disabled]=\"!item.enabled\"\n [class.mtx-calendar-body-active]=\"_isActiveCell(rowIndex, colIndex)\"\n [attr.aria-label]=\"item.ariaLabel\"\n [attr.aria-disabled]=\"!item.enabled || null\"\n (click)=\"_cellClicked(item)\"\n >\n <span\n class=\"mtx-calendar-body-cell-content mat-focus-indicator\"\n [class.mtx-calendar-body-selected]=\"selectedValue === item.value\"\n [class.mtx-calendar-body-today]=\"todayValue === item.value\"\n >\n {{ item.displayValue }}\n </span>\n </button>\n </td>\n }\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-sys-primary))}.mtx-calendar-body-label{height:0;line-height:0;text-align:left;padding-left:4.7142857143%;padding-right:4.7142857143%;font-size:var(--mtx-datetimepicker-calendar-body-label-text-size, var(--mat-sys-title-small-size));font-weight:var(--mtx-datetimepicker-calendar-body-label-text-weight, var(--mat-sys-title-small-weight));color:var(--mtx-datetimepicker-calendar-body-label-text-color, var(--mat-sys-on-surface))}[dir=rtl] .mtx-calendar-body-label{text-align:right}.mtx-calendar-body-week-number{height:0;line-height:0;font-weight:400;color:var(--mtx-datetimepicker-calendar-body-week-number-text-color, var(--mat-sys-secondary))}.mtx-calendar-body-cell-container{position:relative;height:0;line-height:0}.mtx-calendar-body-cell{position:absolute;top:0;left:0;width:100%;height:100%;background:none;text-align:center;margin:0;font-family:var(--mtx-datetimepicker-calendar-text-font, var(--mat-sys-body-medium-font));font-size:var(--mtx-datetimepicker-calendar-text-size, var(--mat-sys-body-medium-size));-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent}.mtx-calendar-body-cell::-moz-focus-inner{border:0}.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, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mtx-calendar-body-disabled>.mtx-calendar-body-today:not(.mtx-calendar-body-selected){border-color:var(--mtx-datetimepicker-calendar-date-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors:active){.mtx-calendar-body-disabled{opacity:.5}}.mtx-calendar-body-cell-content{top:5%;left:5%;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;line-height:1;border:1px;border-style:solid;border-radius:999px;color:var(--mtx-datetimepicker-calendar-date-text-color, var(--mat-sys-on-surface));border-color:var(--mtx-datetimepicker-calendar-date-outline-color, transparent)}.mtx-calendar-body-cell-content.mat-focus-indicator{position:absolute}@media(forced-colors:active){.mtx-calendar-body-cell-content{border:none}}.cdk-keyboard-focused .mtx-calendar-body-active>.mtx-calendar-body-cell-content:not(.mtx-calendar-body-selected),.cdk-program-focused .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, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@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, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}}.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-state-background-color, var(--mat-sys-primary));color:var(--mtx-datetimepicker-calendar-date-selected-state-text-color, var(--mat-sys-on-primary))}.mtx-calendar-body-disabled>.mtx-calendar-body-selected{background-color:var(--mtx-datetimepicker-calendar-date-selected-disabled-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.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-sys-primary))}\n"] }]
}], propDecorators: { label: [{
type: Input
}], rows: [{
type: Input
}], showWeekNumbers: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], cellAspectRatio: [{
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;
let uniqueIdCounter = 0;
/**
* An internal component used to display a single month in the datetimepicker.
* @docs-private
*/
class MtxMonthView {
constructor() {
this._adapter = inject(DatetimeAdapter, { optional: true });
this._dir = inject(Directionality, { optional: true });
this._dateFormats = inject(MTX_DATETIME_FORMATS, { optional: true });
this._intl = inject(MtxDatetimepickerIntl);
this.type = 'date';
/** Whether to show week numbers */
this.showWeekNumbers = false;
/** Emits when a new date is selected. */
this.selectedChange = new EventEmitter();
/** Emits when any date is selected. */
this._userSelection = new EventEmitter();
/** Emits when any date is activated. */
this.activeDateChange = new EventEmitter();
/** The names of the weekdays. */
this._weekdays = [];
this._selected = null;
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 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();
}
}
/** 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();
}
}
/** 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._initWeekdays();
this._createWeekCells();
}
/** Initializes the weekdays. */
_initWeekdays() {
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], id: uniqueIdCounter++ };
});
this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
}
/** 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));
if (this.showWeekNumbers && (cell === 0 || i === 0)) {
const firstDayOfWeek = this._adapter.getFirstDayOfWeek();
const weekNumber = this._adapter.getWeek(date, firstDayOfWeek);
const ariaLabel = this._intl.weekNumberLabel(weekNumber);
this._weeks[this._weeks.length - 1].push(new MtxCalendarCell(weekNumber, `${weekNumber}`, ariaLabel, false, true));
}
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 date && this._adapter.sameMonthAndYear(date, this.activeDate)
? this._adapter.getDate(date)
: null;
}
/** Handles keydown events on the calendar body when calendar is in month view. */
_handleCalendarBodyKeydown(event) {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
const oldActiveDate = this._activeDate;
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, -7);
break;
case DOWN_ARROW:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, 7);
break;
case HOME:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, 1 - this._adapter.getDate(this._activeDate));
break;
case END:
this.activeDate = this._adapter.addCalendarDays(this._activeDate, this._adapter.getNumDaysInMonth(this._activeDate) -
this._adapter.getDate(this._activeDate));
break;
case PAGE_UP:
this.activeDate = event.altKey
? this._adapter.addCalendarYears(this._activeDate, -1)
: this._adapter.addCalendarMonths(this._activeDate, -1);
break;
case PAGE_DOWN:
this.activeDate = event.altKey
? this._adapter.addCalendarYears(this._activeDate, 1)
: this._adapter.addCalendarMonths(this._activeDate, 1);
break;
case ENTER:
case SPACE:
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
this._dateSelected(this._adapter.getDate(this._activeDate));
// this._userSelection.emit();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
return;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
if (this._adapter.compareDate(oldActiveDate, this.activeDate)) {
this.activeDateChange.emit(this.activeDate);
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell(movePreview) {
this._mtxCalendarBody._focusActiveCell(movePreview);
}
/** Determines whether the user has the RTL layout direction. */
_isRtl() {
return this._dir && this._dir.value === 'rtl';
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxMonthView, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MtxMonthView, isStandalone: true, selector: "mtx-month-view", inputs: { type: "type", dateFilter: "dateFilter", showWeekNumbers: ["showWeekNumbers", "showWeekNumbers", booleanAttribute], activeDate: "activeDate", selected: "selected" }, outputs: { selectedChange: "selectedChange", _userSelection: "_userSelection", activeDateChange: "activeDateChange" }, viewQueries: [{ propertyName: "_mtxCalendarBody", first: true, predicate: MtxCalendarBody, descendants: true }], exportAs: ["mtxMonthView"], ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\">\n <tr>\n @if (showWeekNumbers) {\n <th></th>\n }\n @for (day of _weekdays; track day.id) {\n <th [attr.aria-label]=\"day.long\">{{ day.narrow }}</th>\n }\n </tr>\n </thead>\n <tbody\n mtx-calendar-body\n [rows]=\"_weeks\"\n [numCols]=\"showWeekNumbers ? 8 : 7\"\n [showWeekNumbers]=\"showWeekNumbers\"\n [todayValue]=\"_todayDate!\"\n [activeCell]=\"_adapter.getDate(activeDate) - 1\"\n [selectedValue]=\"_selectedDate!\"\n (selectedValueChange)=\"_dateSelected($event)\"\n (keydown)=\"_handleCalendarBodyKeydown($event)\"\n ></tbody>\n</table>\n", dependencies: [{ kind: "component", type: MtxCalendarBody, selector: "[mtx-calendar-body]", inputs: ["label", "rows", "showWeekNumbers", "cellAspectRatio", "todayValue", "selectedValue", "labelMinRequiredCells", "numCols", "allowDisabledSelection", "activeCell"], outputs: ["selectedValueChange"], exportAs: ["mtxCalendarBody"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxMonthView, decorators: [{
type: Component,
args: [{ selector: 'mtx-month-view', exportAs: 'mtxMonthView', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MtxCalendarBody], template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\">\n <tr>\n @if (showWeekNumbers) {\n <th></th>\n }\n @for (day of _weekdays; track day.id) {\n <th [attr.aria-label]=\"day.long\">{{ day.narrow }}</th>\n }\n </tr>\n </thead>\n <tbody\n mtx-calendar-body\n [rows]=\"_weeks\"\n [numCols]=\"showWeekNumbers ? 8 : 7\"\n [showWeekNumbers]=\"showWeekNumbers\"\n [todayValue]=\"_todayDate!\"\n [activeCell]=\"_adapter.getDate(activeDate) - 1\"\n [selectedValue]=\"_selectedDate!\"\n (selectedValueChange)=\"_dateSelected($event)\"\n (keydown)=\"_handleCalendarBodyKeydown($event)\"\n ></tbody>\n</table>\n" }]
}], ctorParameters: () => [], propDecorators: { type: [{
type: Input
}], dateFilter: [{
type: Input
}], showWeekNumbers: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], selectedChange: [{
type: Output
}], _userSelection: [{
type: Output
}], activeDateChange: [{
type: Output
}], _mtxCalendarBody: [{
type: ViewChild,
args: [MtxCalendarBody]
}], 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() {
this._adapter = inject(DatetimeAdapter, { optional: true });
this._dir = inject(Directionality, { optional: true });
this._dateFormats = inject(MTX_DATETIME_FORMATS, { optional: true });
this.type = 'date';
/** Emits when a new month is selected. */
this.selectedChange = new EventEmitter();
/** Emits when any date is selected. */
this._userSelection = new EventEmitter();
/** Emits when any date is activated. */
this.activeDateChange = new EventEmitter();
this._selected = null;
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 dateObject = this._adapter.createDatetime(year, month, Math.min(this._adapter.getDate(this.activeDate), this._adapter.getNumDaysInMonth(normalizedDate)), this._adapter.getHour(this.activeDate), this._adapter.getMinute(this.activeDate));
this.selectedChange.emit(dateObject);
this._activeDate = dateObject;
if (this.type === 'year') {
this._userSelection.emit();
}
}
_getActiveCell() {
return getActiveOffset(this._adapter, this.activeDate, this.minDate, this.maxDate);
}
/** Initializes this year view. */
_init() {
this._todayYear = this._adapter.getYear(this._adapter.today());
this._yearLabel = this._adapter.getYearName(this.activeDate);
const activeYear = this._adapter.getYear(this.activeDate);
const minYearOfPage = activeYear - getActiveOffset(this._adapter, this.activeDate, this.minDate, this.maxDate);
this._years = [];
for (let i = 0, row = []; i < yearsPerPage; i++) {
row.push(minYearOfPage + i);
if (row.length === yearsPerRow) {
this._years.push(row.map(year => this._createCellForYear(year)));
row = [];
}
}
}
/** Creates an MtxCalendarCell for the given year. */
_createCellForYear(year) {
const yearName = this._adapter.getYearName(this._adapter.createDate(year, 0, 1));
return new MtxCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));
}
/** Whether the given year is enabled. */
_shouldEnableYear(year) {
// disable if the year is greater than maxDate lower than minDate
if (year === undefined ||
year === null ||
(this.maxDate && year > this._adapter.getYear(this.maxDate)) ||
(this.minDate && year < this._adapter.getYear(this.minDate))) {
return false;
}
// enable if it reaches here and there's no filter defined
if (!this.dateFilter) {
return true;
}
const firstOfYear = this._adapter.createDate(year, 0, 1);
// If any date in the year is enabled count the year as enabled.
for (let date = firstOfYear; this._adapter.getYear(date) === year; date = this._adapter.addCalendarDays(date, 1)) {
if (this.dateFilter(date)) {
return true;
}
}
return false;
}
/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
*/
_getValidDateOrNull(obj) {
return this._adapter.isDateInstance(obj) && this._adapter.isValid(obj) ? obj : null;
}
/** Handles keydown events on the calendar body when calendar is in multi-year view. */
_handleCalendarBodyKeydown(event) {
const oldActiveDate = this._activeDate;
const isRtl = this._isRtl();
switch (event.keyCode) {
case LEFT_ARROW:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, isRtl ? 1 : -1);
break;
case RIGHT_ARROW:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, isRtl ? -1 : 1);
break;
case UP_ARROW:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, -yearsPerRow);
break;
case DOWN_ARROW:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, yearsPerRow);
break;
case HOME:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, -getActiveOffset(this._adapter, this._activeDate, this.minDate, this.maxDate));
break;
case END:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, yearsPerPage -
getActiveOffset(this._adapter, this._activeDate, this.minDate, this.maxDate) -
1);
break;
case PAGE_UP:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, event.altKey ? -yearsPerPage * 10 : -yearsPerPage);
break;
case PAGE_DOWN:
this.activeDate = this._adapter.addCalendarYears(this._activeDate, event.altKey ? yearsPerPage * 10 : yearsPerPage);
break;
case ENTER:
case SPACE:
this._yearSelected(this._adapter.getYear(this._activeDate));
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
if (this._adapter.compareDate(oldActiveDate, this.activeDate)) {
this.activeDateChange.emit(this.activeDate);
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell() {
this._mtxCalendarBody._focusActiveCell();
}
/** Determines whether the user has the RTL layout direction. */
_isRtl() {
return this._dir && this._dir.value === 'rtl';
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxMultiYearView, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.5", type: MtxMultiYearView, isStandalone: true, selector: "mtx-multi-year-view", inputs: { type: "type", dateFilter: "dateFilter", activeDate: "activeDate", selected: "selected", minDate: "minDate", maxDate: "maxDate" }, outputs: { selectedChange: "selectedChange", _userSelection: "_userSelection", activeDateChange: "activeDateChange" }, viewQueries: [{ propertyName: "_mtxCalendarBody", first: true, predicate: MtxCalendarBody, descendants: true }], exportAs: ["mtxMultiYearView"], ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\"></thead>\n <tbody mtx-calendar-body\n [rows]=\"_years\"\n [todayValue]=\"_todayYear\"\n [numCols]=\"4\"\n [cellAspectRatio]=\"4 / 7\"\n [activeCell]=\"_getActiveCell()\"\n [allowDisabledSelection]=\"true\"\n [selectedValue]=\"_selectedYear!\"\n (selectedValueChange)=\"_yearSelected($event)\"\n (keydown)=\"_handleCalendarBodyKeydown($event)\"></tbody>\n</table>\n", dependencies: [{ kind: "component", type: MtxCalendarBody, selector: "[mtx-calendar-body]", inputs: ["label", "rows", "showWeekNumbers", "cellAspectRatio", "todayValue", "selectedValue", "labelMinRequiredCells", "numCols", "allowDisabledSelection", "activeCell"], outputs: ["selectedValueChange"], exportAs: ["mtxCalendarBody"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MtxMultiYearView, decorators: [{
type: Component,
args: [{ selector: 'mtx-multi-year-view', exportAs: 'mtxMultiYearView', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, imports: [MtxCalendarBody], template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\"></thead>\n <tbody mtx-calendar-body\n [rows]=\"_years\"\n [todayValue]=\"_todayYear\"\n [numCols]=\"4\"\n [cellAspectRatio]=\"4 / 7\"\n [activeCell]=\"_getActiveCell()\"\n [allowDisabledSelection]=\"true\"\n [selectedValue]=\"_selectedYear!\"\n (selectedValueChange)=\"_yearSelected($event)\"\n (keydown)=\"_handleCalendarBodyKeydown($event)\"></tbody>\n</table>\n" }]
}], ctorParameters: () => [], propDecorators: { type: [{
type: Input
}], dateFilter: [{
type: Input
}], selectedChange: [{
type: Output
}], _userSelection: [{
type: Output
}], activeDateChange: [{
type: Output
}], _mtxCalendarBody: [{
type: ViewChild,
args: [MtxCalendarBody]
}], activeDate: [{
type: Input
}], selected: [{
type: Input