@ng-matero/extensions
Version:
Angular Material Extensions
1 lines • 264 kB
Source Map (JSON)
{"version":3,"file":"mtxDatetimepicker.mjs","sources":["../../../projects/extensions/datetimepicker/datetimepicker-filtertype.ts","../../../projects/extensions/datetimepicker/clock.ts","../../../projects/extensions/datetimepicker/clock.html","../../../projects/extensions/datetimepicker/datetimepicker-animations.ts","../../../projects/extensions/datetimepicker/datetimepicker-errors.ts","../../../projects/extensions/datetimepicker/calendar-body.ts","../../../projects/extensions/datetimepicker/calendar-body.html","../../../projects/extensions/datetimepicker/month-view.ts","../../../projects/extensions/datetimepicker/month-view.html","../../../projects/extensions/datetimepicker/multi-year-view.ts","../../../projects/extensions/datetimepicker/multi-year-view.html","../../../projects/extensions/datetimepicker/datetimepicker-intl.ts","../../../projects/extensions/datetimepicker/time.ts","../../../projects/extensions/datetimepicker/time.html","../../../projects/extensions/datetimepicker/year-view.ts","../../../projects/extensions/datetimepicker/year-view.html","../../../projects/extensions/datetimepicker/calendar.ts","../../../projects/extensions/datetimepicker/calendar.html","../../../projects/extensions/datetimepicker/datetimepicker.ts","../../../projects/extensions/datetimepicker/datetimepicker-content.html","../../../projects/extensions/datetimepicker/datetimepicker-input.ts","../../../projects/extensions/datetimepicker/datetimepicker-toggle.ts","../../../projects/extensions/datetimepicker/datetimepicker-toggle.html","../../../projects/extensions/datetimepicker/datetimepicker-actions.ts","../../../projects/extensions/datetimepicker/datetimepicker-module.ts","../../../projects/extensions/datetimepicker/mtxDatetimepicker.ts"],"sourcesContent":["export enum MtxDatetimepickerFilterType {\n DATE,\n HOUR,\n MINUTE,\n}\n","import { normalizePassiveListenerOptions } from '@angular/cdk/platform';\nimport { DOCUMENT } from '@angular/common';\nimport {\n AfterContentInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ElementRef,\n EventEmitter,\n Inject,\n Input,\n OnChanges,\n OnDestroy,\n Output,\n ViewEncapsulation,\n booleanAttribute,\n} from '@angular/core';\n\nimport { DatetimeAdapter } from '@ng-matero/extensions/core';\nimport { MtxDatetimepickerFilterType } from './datetimepicker-filtertype';\nimport { MtxAMPM } from './datetimepicker-types';\n\nconst activeEventOptions = normalizePassiveListenerOptions({ passive: false });\n\nexport const CLOCK_RADIUS = 50;\nexport const CLOCK_INNER_RADIUS = 27.5;\nexport const CLOCK_OUTER_RADIUS = 41.25;\nexport const CLOCK_TICK_RADIUS = 7.0833;\n\n/** Possible views for datetimepicker clock. */\nexport type MtxClockView = 'hour' | 'minute';\n\n/**\n * A clock that is used as part of the datetimepicker.\n * @docs-private\n */\n@Component({\n selector: 'mtx-clock',\n templateUrl: 'clock.html',\n styleUrl: 'clock.scss',\n host: {\n 'role': 'clock',\n 'class': 'mtx-clock',\n '(mousedown)': '_pointerDown($event)',\n '(touchstart)': '_pointerDown($event)',\n },\n exportAs: 'mtxClock',\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n})\nexport class MtxClock<D> implements AfterContentInit, OnDestroy, OnChanges {\n /** A function used to filter which dates are selectable. */\n @Input() dateFilter!: (date: D, type: MtxDatetimepickerFilterType) => boolean;\n\n /** Step over minutes. */\n @Input() interval: number = 1;\n\n /** Whether the clock uses 12 hour format. */\n @Input({ transform: booleanAttribute }) twelvehour: boolean = false;\n\n /** Whether the time is now in AM or PM. */\n @Input() AMPM: MtxAMPM = 'AM';\n\n /** Emits when the currently selected date changes. */\n @Output() selectedChange = new EventEmitter<D>();\n\n /** Emits when any date is activated. */\n @Output() activeDateChange = new EventEmitter<D>();\n\n /** Emits when any date is selected. */\n @Output() readonly _userSelection = new EventEmitter<void>();\n\n /** Whether the clock is in hour view. */\n _hourView: boolean = true;\n\n _hours: any[] = [];\n\n _minutes: any[] = [];\n\n _selectedHour!: number;\n\n _selectedMinute!: number;\n\n private _timeChanged = false;\n\n constructor(\n private _elementRef: ElementRef,\n private _adapter: DatetimeAdapter<D>,\n private _changeDetectorRef: ChangeDetectorRef,\n @Inject(DOCUMENT) private _document: any\n ) {}\n\n /**\n * The date to display in this clock view.\n */\n @Input()\n get activeDate(): D {\n return this._activeDate;\n }\n set activeDate(value: D) {\n const oldActiveDate = this._activeDate;\n this._activeDate = this._adapter.clampDate(value, this.minDate, this.maxDate);\n if (!this._adapter.sameMinute(oldActiveDate, this._activeDate)) {\n this._init();\n }\n }\n private _activeDate!: D;\n\n /** The currently selected date. */\n @Input()\n get selected(): D | null {\n return this._selected;\n }\n set selected(value: D | null) {\n this._selected = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));\n if (this._selected) {\n this.activeDate = this._selected;\n }\n }\n private _selected!: D | null;\n\n /** The minimum selectable date. */\n @Input()\n get minDate(): D | null {\n return this._minDate;\n }\n set minDate(value: D | null) {\n this._minDate = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));\n }\n private _minDate!: D | null;\n\n /** The maximum selectable date. */\n @Input()\n get maxDate(): D | null {\n return this._maxDate;\n }\n set maxDate(value: D | null) {\n this._maxDate = this._adapter.getValidDateOrNull(this._adapter.deserialize(value));\n }\n private _maxDate!: D | null;\n\n /** Whether the clock should be started in hour or minute view. */\n @Input()\n set startView(value: MtxClockView) {\n this._hourView = value !== 'minute';\n }\n\n get _hand() {\n const hour = this._adapter.getHour(this.activeDate);\n this._selectedHour = hour;\n this._selectedMinute = this._adapter.getMinute(this.activeDate);\n let deg = 0;\n let radius = CLOCK_OUTER_RADIUS;\n if (this._hourView) {\n const outer = this._selectedHour > 0 && this._selectedHour < 13;\n radius = outer ? CLOCK_OUTER_RADIUS : CLOCK_INNER_RADIUS;\n if (this.twelvehour) {\n radius = CLOCK_OUTER_RADIUS;\n }\n deg = Math.round(this._selectedHour * (360 / (24 / 2)));\n } else {\n deg = Math.round(this._selectedMinute * (360 / 60));\n }\n return {\n height: `${radius}%`,\n marginTop: `${50 - radius}%`,\n transform: `rotate(${deg}deg)`,\n };\n }\n\n ngAfterContentInit() {\n this.activeDate = this._activeDate || this._adapter.today();\n this._init();\n }\n\n ngOnDestroy() {\n this._removeGlobalEvents();\n }\n\n ngOnChanges(): void {\n this._init();\n }\n\n /** Called when the user has put their pointer down on the clock. */\n private _pointerDown = (event: TouchEvent | MouseEvent) => {\n this._timeChanged = false;\n this.setTime(event);\n this._bindGlobalEvents(event);\n };\n\n /**\n * Called when the user has moved their pointer after\n * starting to drag. Bound on the document level.\n */\n private _pointerMove = (event: TouchEvent | MouseEvent) => {\n if (event.cancelable) {\n event.preventDefault();\n }\n this.setTime(event);\n };\n\n /** Called when the user has lifted their pointer. Bound on the document level. */\n private _pointerUp = (event: TouchEvent | MouseEvent) => {\n if (event.cancelable) {\n event.preventDefault();\n }\n this._removeGlobalEvents();\n\n if (this._timeChanged) {\n this.selectedChange.emit(this.activeDate);\n if (!this._hourView) {\n this._userSelection.emit();\n }\n }\n };\n\n /** Binds our global move and end events. */\n private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {\n // Note that we bind the events to the `document`, because it allows us to capture\n // drag cancel events where the user's pointer is outside the browser window.\n const document = this._document;\n const isTouch = isTouchEvent(triggerEvent);\n const moveEventName = isTouch ? 'touchmove' : 'mousemove';\n const endEventName = isTouch ? 'touchend' : 'mouseup';\n document.addEventListener(moveEventName, this._pointerMove, activeEventOptions);\n document.addEventListener(endEventName, this._pointerUp, activeEventOptions);\n\n if (isTouch) {\n document.addEventListener('touchcancel', this._pointerUp, activeEventOptions);\n }\n }\n\n /** Removes any global event listeners that we may have added. */\n private _removeGlobalEvents() {\n const document = this._document;\n document.removeEventListener('mousemove', this._pointerMove, activeEventOptions);\n document.removeEventListener('mouseup', this._pointerUp, activeEventOptions);\n document.removeEventListener('touchmove', this._pointerMove, activeEventOptions);\n document.removeEventListener('touchend', this._pointerUp, activeEventOptions);\n document.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);\n }\n\n /** Initializes this clock view. */\n private _init() {\n this._hours.length = 0;\n this._minutes.length = 0;\n\n const hourNames = this._adapter.getHourNames();\n const minuteNames = this._adapter.getMinuteNames();\n if (this.twelvehour) {\n const hours = [];\n for (let i = 0; i < hourNames.length; i++) {\n const radian = (i / 6) * Math.PI;\n const radius = CLOCK_OUTER_RADIUS;\n\n const hour = i;\n const date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n this._adapter.getDate(this.activeDate),\n hour,\n 0\n );\n\n // Check if the date is enabled, no need to respect the minute setting here\n const enabled =\n (!this.minDate ||\n (this._adapter.compareDatetime(date, this.minDate, false) as number) >= 0) &&\n (!this.maxDate ||\n (this._adapter.compareDatetime(date, this.maxDate, false) as number) <= 0) &&\n (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR));\n\n // display value for twelvehour clock should be from 1-12 not including 0 and not above 12\n hours.push({\n value: i,\n displayValue: i % 12 === 0 ? '12' : hourNames[i % 12],\n enabled,\n top: CLOCK_RADIUS - Math.cos(radian) * radius - CLOCK_TICK_RADIUS,\n left: CLOCK_RADIUS + Math.sin(radian) * radius - CLOCK_TICK_RADIUS,\n });\n }\n\n // filter out AM or PM hours based on AMPM\n if (this.AMPM === 'AM') {\n this._hours = hours.filter(x => x.value < 12);\n } else {\n this._hours = hours.filter(x => x.value >= 12);\n }\n } else {\n for (let i = 0; i < hourNames.length; i++) {\n const radian = (i / 6) * Math.PI;\n const outer = i > 0 && i < 13;\n const radius = outer ? CLOCK_OUTER_RADIUS : CLOCK_INNER_RADIUS;\n const date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n this._adapter.getDate(this.activeDate),\n i,\n 0\n );\n\n // Check if the date is enabled, no need to respect the minute setting here\n const enabled =\n (!this.minDate ||\n (this._adapter.compareDatetime(date, this.minDate, false) as number) >= 0) &&\n (!this.maxDate ||\n (this._adapter.compareDatetime(date, this.maxDate, false) as number) <= 0) &&\n (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.HOUR));\n\n this._hours.push({\n value: i,\n displayValue: i === 0 ? '00' : hourNames[i],\n enabled,\n top: CLOCK_RADIUS - Math.cos(radian) * radius - CLOCK_TICK_RADIUS,\n left: CLOCK_RADIUS + Math.sin(radian) * radius - CLOCK_TICK_RADIUS,\n fontSize: i > 0 && i < 13 ? '' : '80%',\n });\n }\n }\n\n for (let i = 0; i < minuteNames.length; i += 5) {\n const radian = (i / 30) * Math.PI;\n const date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n this._adapter.getDate(this.activeDate),\n this._adapter.getHour(this.activeDate),\n i\n );\n const enabled =\n (!this.minDate || (this._adapter.compareDatetime(date, this.minDate) as number) >= 0) &&\n (!this.maxDate || (this._adapter.compareDatetime(date, this.maxDate) as number) <= 0) &&\n (!this.dateFilter || this.dateFilter(date, MtxDatetimepickerFilterType.MINUTE));\n this._minutes.push({\n value: i,\n displayValue: i === 0 ? '00' : minuteNames[i],\n enabled,\n top: CLOCK_RADIUS - Math.cos(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS,\n left: CLOCK_RADIUS + Math.sin(radian) * CLOCK_OUTER_RADIUS - CLOCK_TICK_RADIUS,\n });\n }\n }\n\n /**\n * Set Time\n * @param event\n */\n private setTime(event: TouchEvent | MouseEvent) {\n const trigger = this._elementRef.nativeElement;\n const triggerRect = trigger.getBoundingClientRect();\n const width = trigger.offsetWidth;\n const height = trigger.offsetHeight;\n const { pageX, pageY } = getPointerPositionOnPage(event);\n const x = width / 2 - (pageX - triggerRect.left - window.pageXOffset);\n const y = height / 2 - (pageY - triggerRect.top - window.pageYOffset);\n\n let radian = Math.atan2(-x, y);\n const unit = Math.PI / (this._hourView ? 6 : this.interval ? 30 / this.interval : 30);\n const z = Math.sqrt(x * x + y * y);\n const outer =\n this._hourView &&\n z > (width * (CLOCK_OUTER_RADIUS / 100) + width * (CLOCK_INNER_RADIUS / 100)) / 2;\n\n if (radian < 0) {\n radian = Math.PI * 2 + radian;\n }\n let value = Math.round(radian / unit);\n\n let date;\n if (this._hourView) {\n if (this.twelvehour) {\n if (this.AMPM === 'AM') {\n value = value === 0 ? 12 : value;\n } else {\n // if we chosen 12 in PM, the value should be 0 for 0:00,\n // else we can safely add 12 to the final value\n value = value === 12 ? 0 : value + 12;\n }\n } else {\n if (value === 12) {\n value = 0;\n }\n value = outer ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12;\n }\n\n date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n this._adapter.getDate(this.activeDate),\n value,\n this._adapter.getMinute(this.activeDate)\n );\n } else {\n if (this.interval) {\n value *= this.interval;\n }\n if (value === 60) {\n value = 0;\n }\n date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n this._adapter.getDate(this.activeDate),\n this._adapter.getHour(this.activeDate),\n value\n );\n }\n\n // if there is a dateFilter, check if the date is allowed if it is not then do not set/emit new date\n // https://github.com/ng-matero/extensions/issues/244\n if (\n this.dateFilter &&\n !this.dateFilter(\n date,\n this._hourView ? MtxDatetimepickerFilterType.HOUR : MtxDatetimepickerFilterType.MINUTE\n )\n ) {\n return;\n }\n\n this._timeChanged = true;\n this.activeDate = date;\n this._changeDetectorRef.markForCheck();\n this.activeDateChange.emit(this.activeDate);\n }\n}\n\n/** Returns whether an event is a touch event. */\nfunction isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {\n // This function is called for every pixel that the user has dragged so we need it to be\n // as fast as possible. Since we only bind mouse events and touch events, we can assume\n // that if the event's name starts with `t`, it's a touch event.\n return event.type[0] === 't';\n}\n\n/** Gets the coordinates of a touch or mouse event relative to the document. */\nfunction getPointerPositionOnPage(event: MouseEvent | TouchEvent) {\n let point: { pageX: number; pageY: number };\n\n if (isTouchEvent(event)) {\n // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.\n point = event.touches[0] || event.changedTouches[0];\n } else {\n point = event;\n }\n\n return point;\n}\n","<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","import {\n animate,\n AnimationTriggerMetadata,\n keyframes,\n state,\n style,\n transition,\n trigger,\n} from '@angular/animations';\n\n/**\n * Animations used by the Material datetimepicker.\n * @docs-private\n */\nexport const mtxDatetimepickerAnimations: {\n readonly transformPanel: AnimationTriggerMetadata;\n readonly fadeInCalendar: AnimationTriggerMetadata;\n readonly slideCalendar: AnimationTriggerMetadata;\n} = {\n /** Transforms the height of the datetimepicker's calendar. */\n transformPanel: trigger('transformPanel', [\n transition(\n 'void => enter-dropdown',\n animate(\n '120ms cubic-bezier(0, 0, 0.2, 1)',\n keyframes([\n style({ opacity: 0, transform: 'scale(1, 0.8)' }),\n style({ opacity: 1, transform: 'scale(1, 1)' }),\n ])\n )\n ),\n transition(\n 'void => enter-dialog',\n animate(\n '150ms cubic-bezier(0, 0, 0.2, 1)',\n keyframes([\n style({ opacity: 0, transform: 'scale(0.7)' }),\n style({ transform: 'none', opacity: 1 }),\n ])\n )\n ),\n transition('* => void', animate('100ms linear', style({ opacity: 0 }))),\n ]),\n\n /** Fades in the content of the calendar. */\n fadeInCalendar: trigger('fadeInCalendar', [\n state('void', style({ opacity: 0 })),\n state('enter', style({ opacity: 1 })),\n\n // TODO(crisbeto): this animation should be removed since it isn't quite on spec, but we\n // need to keep it until #12440 gets in, otherwise the exit animation will look glitchy.\n transition('void => *', animate('120ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')),\n ]),\n\n slideCalendar: trigger('slideCalendar', [\n transition('* => left', [\n animate(\n 180,\n keyframes([\n style({ transform: 'translateX(100%)', offset: 0.5 }),\n style({ transform: 'translateX(-100%)', offset: 0.51 }),\n style({ transform: 'translateX(0)', offset: 1 }),\n ])\n ),\n ]),\n transition('* => right', [\n animate(\n 180,\n keyframes([\n style({ transform: 'translateX(-100%)', offset: 0.5 }),\n style({ transform: 'translateX(100%)', offset: 0.51 }),\n style({ transform: 'translateX(0)', offset: 1 }),\n ])\n ),\n ]),\n ]),\n};\n","/** @docs-private */\nexport function createMissingDateImplError(provider: string) {\n return Error(\n `MtxDatetimepicker: No provider found for ${provider}. You must add one of the following ` +\n `to your app config: provideNativeDatetimeAdapter, provideDateFnsDatetimeAdapter,` +\n `provideLuxonDatetimeAdapter, provideMomentDatetimeAdapter, or provide a ` +\n `custom implementation.`\n );\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n EventEmitter,\n Input,\n Output,\n ViewEncapsulation,\n} from '@angular/core';\n\n/**\n * An internal class that represents the data corresponding to a single calendar cell.\n * @docs-private\n */\nexport class MtxCalendarCell {\n constructor(\n public value: number,\n public displayValue: string,\n public ariaLabel: string,\n public enabled: boolean\n ) {}\n}\n\n/**\n * An internal component used to display calendar data in a table.\n * @docs-private\n */\n@Component({\n selector: '[mtx-calendar-body]',\n templateUrl: 'calendar-body.html',\n styleUrl: 'calendar-body.scss',\n host: {\n class: 'mtx-calendar-body',\n },\n exportAs: 'mtxCalendarBody',\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n})\nexport class MtxCalendarBody {\n /** The label for the table. (e.g. \"Jan 2017\"). */\n @Input() label!: string;\n\n /** The cells to display in the table. */\n @Input() rows!: MtxCalendarCell[][];\n\n /** The value in the table that corresponds to today. */\n @Input() todayValue!: number;\n\n /** The value in the table that is currently selected. */\n @Input() selectedValue!: number;\n\n /** The minimum number of free cells needed to fit the label in the first row. */\n @Input() labelMinRequiredCells!: number;\n\n /** The number of columns in the table. */\n @Input() numCols = 7;\n\n /** Whether to allow selection of disabled cells. */\n @Input() allowDisabledSelection = false;\n\n /** The cell number of the active cell in the table. */\n @Input() activeCell = 0;\n\n /** Emits when a new value is selected. */\n @Output() selectedValueChange = new EventEmitter<number>();\n\n /** The number of blank cells to put at the beginning for the first row. */\n get _firstRowOffset(): number {\n return this.rows && this.rows.length && this.rows[0].length\n ? this.numCols - this.rows[0].length\n : 0;\n }\n\n _cellClicked(cell: MtxCalendarCell): void {\n if (!this.allowDisabledSelection && !cell.enabled) {\n return;\n }\n this.selectedValueChange.emit(cell.value);\n }\n\n _isActiveCell(rowIndex: number, colIndex: number): boolean {\n let cellNumber = rowIndex * this.numCols + colIndex;\n\n // Account for the fact that the first row may not have as many cells.\n if (rowIndex) {\n cellNumber -= this._firstRowOffset;\n }\n\n return cellNumber === this.activeCell;\n }\n}\n","<!--\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","import {\n AfterContentInit,\n ChangeDetectionStrategy,\n Component,\n EventEmitter,\n Inject,\n Input,\n Optional,\n Output,\n ViewEncapsulation,\n} from '@angular/core';\nimport {\n DatetimeAdapter,\n MTX_DATETIME_FORMATS,\n MtxDatetimeFormats,\n} from '@ng-matero/extensions/core';\nimport { MtxCalendarBody, MtxCalendarCell } from './calendar-body';\nimport { mtxDatetimepickerAnimations } from './datetimepicker-animations';\nimport { createMissingDateImplError } from './datetimepicker-errors';\nimport { MtxDatetimepickerType } from './datetimepicker-types';\n\nconst DAYS_PER_WEEK = 7;\n\n/**\n * An internal component used to display a single month in the datetimepicker.\n * @docs-private\n */\n@Component({\n selector: 'mtx-month-view',\n templateUrl: 'month-view.html',\n exportAs: 'mtxMonthView',\n animations: [mtxDatetimepickerAnimations.slideCalendar],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n imports: [MtxCalendarBody],\n})\nexport class MtxMonthView<D> implements AfterContentInit {\n @Input() type: MtxDatetimepickerType = 'date';\n\n /** A function used to filter which dates are selectable. */\n @Input() dateFilter!: (date: D) => boolean;\n\n /** Emits when a new date is selected. */\n @Output() selectedChange = new EventEmitter<D>();\n\n /** Emits when any date is selected. */\n @Output() readonly _userSelection = new EventEmitter<void>();\n\n /** Grid of calendar cells representing the dates of the month. */\n _weeks!: MtxCalendarCell[][];\n\n /** The number of blank cells in the first row before the 1st of the month. */\n _firstWeekOffset!: number;\n\n /**\n * The date of the month that the currently selected Date falls on.\n * Null if the currently selected Date is in another month.\n */\n _selectedDate!: number | null;\n\n /** The date of the month that today falls on. Null if today is in another month. */\n _todayDate!: number | null;\n\n /** The names of the weekdays. */\n _weekdays: { long: string; narrow: string }[];\n\n _calendarState!: string;\n\n constructor(\n @Optional() public _adapter: DatetimeAdapter<D>,\n @Optional() @Inject(MTX_DATETIME_FORMATS) private _dateFormats: MtxDatetimeFormats\n ) {\n if (!this._adapter) {\n throw createMissingDateImplError('DatetimeAdapter');\n }\n\n if (!this._dateFormats) {\n throw createMissingDateImplError('MTX_DATETIME_FORMATS');\n }\n\n const firstDayOfWeek = this._adapter.getFirstDayOfWeek();\n const narrowWeekdays = this._adapter.getDayOfWeekNames('narrow');\n const longWeekdays = this._adapter.getDayOfWeekNames('long');\n\n // Rotate the labels for days of the week based on the configured first day of the week.\n const weekdays = longWeekdays.map((long, i) => {\n return { long, narrow: narrowWeekdays[i] };\n });\n this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));\n\n this._activeDate = this._adapter.today();\n }\n\n private _activeDate: D;\n\n /**\n * The date to display in this month view (everything other than the month and year is ignored).\n */\n @Input()\n get activeDate(): D {\n return this._activeDate;\n }\n\n set activeDate(value: D) {\n const oldActiveDate = this._activeDate;\n this._activeDate = value || this._adapter.today();\n if (\n oldActiveDate &&\n this._activeDate &&\n !this._adapter.sameMonthAndYear(oldActiveDate, this._activeDate)\n ) {\n this._init();\n if (this._adapter.isInNextMonth(oldActiveDate, this._activeDate)) {\n this.calendarState('right');\n } else {\n this.calendarState('left');\n }\n }\n }\n\n /** The currently selected date. */\n @Input()\n get selected(): D {\n return this._selected;\n }\n set selected(value: D) {\n this._selected = value;\n this._selectedDate = this._getDateInCurrentMonth(this.selected);\n }\n private _selected!: D;\n\n ngAfterContentInit(): void {\n this._init();\n }\n\n /** Handles when a new date is selected. */\n _dateSelected(date: number) {\n const dateObject = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n date,\n this._adapter.getHour(this.activeDate),\n this._adapter.getMinute(this.activeDate)\n );\n this.selectedChange.emit(dateObject);\n this._activeDate = dateObject;\n\n if (this.type === 'date') {\n this._userSelection.emit();\n }\n }\n\n _calendarStateDone() {\n this._calendarState = '';\n }\n\n /** Initializes this month view. */\n private _init() {\n this._selectedDate = this._getDateInCurrentMonth(this.selected);\n this._todayDate = this._getDateInCurrentMonth(this._adapter.today());\n\n const firstOfMonth = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n 1,\n this._adapter.getHour(this.activeDate),\n this._adapter.getMinute(this.activeDate)\n );\n this._firstWeekOffset =\n (DAYS_PER_WEEK +\n this._adapter.getDayOfWeek(firstOfMonth) -\n this._adapter.getFirstDayOfWeek()) %\n DAYS_PER_WEEK;\n\n this._createWeekCells();\n }\n\n /** Creates MdCalendarCells for the dates in this month. */\n private _createWeekCells() {\n const daysInMonth = this._adapter.getNumDaysInMonth(this.activeDate);\n const dateNames = this._adapter.getDateNames();\n this._weeks = [[]];\n for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {\n if (cell === DAYS_PER_WEEK) {\n this._weeks.push([]);\n cell = 0;\n }\n const date = this._adapter.createDatetime(\n this._adapter.getYear(this.activeDate),\n this._adapter.getMonth(this.activeDate),\n i + 1,\n this._adapter.getHour(this.activeDate),\n this._adapter.getMinute(this.activeDate)\n );\n const enabled = !this.dateFilter || this.dateFilter(date);\n const ariaLabel = this._adapter.format(date, this._dateFormats.display.dateA11yLabel);\n this._weeks[this._weeks.length - 1].push(\n new MtxCalendarCell(i + 1, dateNames[i], ariaLabel, enabled)\n );\n }\n }\n\n /**\n * Gets the date in this month that the given Date falls on.\n * Returns null if the given Date is in another month.\n */\n private _getDateInCurrentMonth(date: D): number | null {\n return this._adapter.sameMonthAndYear(date, this.activeDate)\n ? this._adapter.getDate(date)\n : null;\n }\n\n private calendarState(direction: string): void {\n this._calendarState = direction;\n }\n}\n","<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","import {\n AfterContentInit,\n ChangeDetectionStrategy,\n Component,\n EventEmitter,\n Inject,\n Input,\n Optional,\n Output,\n ViewEncapsulation,\n} from '@angular/core';\nimport {\n DatetimeAdapter,\n MTX_DATETIME_FORMATS,\n MtxDatetimeFormats,\n} from '@ng-matero/extensions/core';\nimport { MtxCalendarBody, MtxCalendarCell } from './calendar-body';\nimport { mtxDatetimepickerAnimations } from './datetimepicker-animations';\nimport { createMissingDateImplError } from './datetimepicker-errors';\nimport { MtxDatetimepickerType } from './datetimepicker-types';\n\nexport const yearsPerPage = 24;\n\nexport const yearsPerRow = 4;\n\n/**\n * An internal component used to display multiple years in the datetimepicker.\n * @docs-private\n */\n@Component({\n selector: 'mtx-multi-year-view',\n templateUrl: 'multi-year-view.html',\n exportAs: 'mtxMultiYearView',\n animations: [mtxDatetimepickerAnimations.slideCalendar],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n imports: [MtxCalendarBody],\n})\nexport class MtxMultiYearView<D> implements AfterContentInit {\n @Input() type: MtxDatetimepickerType = 'date';\n\n /** A function used to filter which dates are selectable. */\n @Input() dateFilter!: (date: D) => boolean;\n\n /** Emits when a new month is selected. */\n @Output() selectedChange = new EventEmitter<D>();\n\n /** Emits when any date is selected. */\n @Output() readonly _userSelection = new EventEmitter<void>();\n\n /** Grid of calendar cells representing the years in the range. */\n _years!: MtxCalendarCell[][];\n\n /** The label for this year range (e.g. \"2000-2020\"). */\n _yearLabel!: string;\n\n /** The year in this range that today falls on. Null if today is in a different range. */\n _todayYear!: number;\n\n /**\n * The year in this range that the selected Date falls on.\n * Null if the selected Date is in a different range.\n */\n _selectedYear!: number | null;\n\n _calendarState!: string;\n\n constructor(\n @Optional() public _adapter: DatetimeAdapter<D>,\n @Optional() @Inject(MTX_DATETIME_FORMATS) private _dateFormats: MtxDatetimeFormats\n ) {\n if (!this._adapter) {\n throw createMissingDateImplError('DatetimeAdapter');\n }\n\n if (!this._dateFormats) {\n throw createMissingDateImplError('MTX_DATETIME_FORMATS');\n }\n\n this._activeDate = this._adapter.today();\n }\n\n /** The date to display in this multi year view */\n @Input()\n get activeDate(): D {\n return this._activeDate;\n }\n set activeDate(value: D) {\n const oldActiveDate = this._activeDate;\n this._activeDate = value || this._adapter.today();\n if (\n oldActiveDate &&\n this._activeDate &&\n !isSameMultiYearView(\n this._adapter,\n oldActiveDate,\n this._activeDate,\n this.minDate,\n this.maxDate\n )\n ) {\n this._init();\n }\n }\n private _activeDate: D;\n\n /** The currently selected date. */\n @Input()\n get selected(): D {\n return this._selected;\n }\n set selected(value: D) {\n this._selected = value;\n this._selectedYear = this._selected && this._adapter.getYear(this._selected);\n }\n private _selected!: D;\n\n /** The minimum selectable date. */\n @Input()\n get minDate(): D | null {\n return this._minDate;\n }\n set minDate(value: D | null) {\n this._minDate = this._getValidDateOrNull(this._adapter.deserialize(value));\n }\n private _minDate!: D | null;\n\n /** The maximum selectable date. */\n @Input()\n get maxDate(): D | null {\n return this._maxDate;\n }\n set maxDate(value: D | null) {\n this._maxDate = this._getValidDateOrNull(this._adapter.deserialize(value));\n }\n private _maxDate!: D | null;\n\n ngAfterContentInit() {\n this._init();\n }\n\n /** Handles when a new year is selected. */\n _yearSelected(year: number) {\n const month = this._adapter.getMonth(this.activeDate);\n const normalizedDate = this._adapter.createDatetime(year, month, 1, 0, 0);\n\n const dateObject = this._adapter.createDatetime(\n year,\n month,\n Math.min(\n this._adapter.getDate(this.activeDate),\n this._adapter.getNumDaysInMonth(normalizedDate)\n ),\n this._adapter.getHour(this.activeDate),\n this._adapter.getMinute(this.activeDate)\n );\n\n this.selectedChange.emit(dateObject);\n this._activeDate = dateObject;\n\n if (this.type === 'year') {\n this._userSelection.emit();\n }\n }\n\n _getActiveCell(): number {\n return getActiveOffset(this._adapter, this.activeDate, this.minDate, this.maxDate);\n }\n\n _calendarStateDone() {\n this._calendarState = '';\n }\n\n /** Initializes this year view. */\n private _init() {\n this._todayYear = this._adapter.getYear(this._adapter.today());\n this._yearLabel = this._adapter.getYearName(this.activeDate);\n\n const activeYear = this._adapter.getYear(this.activeDate);\n\n const minYearOfPage =\n activeYear - getActiveOffset(this._adapter, this.activeDate, this.minDate, this.maxDate);\n\n this._years = [];\n for (let i = 0, row: number[] = []; i < yearsPerPage; i++) {\n row.push(minYearOfPage + i);\n if (row.length === yearsPerRow) {\n this._years.push(row.map(year => this._createCellForYear(year)));\n row = [];\n }\n }\n }\n\n /** Creates an MtxCalendarCell for the given year. */\n private _createCellForYear(year: number) {\n const yearName = this._adapter.getYearName(this._adapter.createDate(year, 0, 1));\n return new MtxCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));\n }\n\n /** Whether the given year is enabled. */\n private _shouldEnableYear(year: number) {\n // disable if the year is greater than maxDate lower than minDate\n if (\n year === undefined ||\n year === null ||\n (this.maxDate && year > this._adapter.getYear(this.maxDate)) ||\n (this.minDate && year < this._adapter.getYear(this.minDate))\n ) {\n return false;\n }\n\n // enable if it reaches here and there's no filter defined\n if (!this.dateFilter) {\n return true;\n }\n\n const firstOfYear = this._adapter.createDate(year, 0, 1);\n\n // If any date in the year is enabled count the year as enabled.\n for (\n let date = firstOfYear;\n this._adapter.getYear(date) === year;\n date = this._adapter.addCalendarDays(date, 1)\n ) {\n if (this.dateFilter(date)) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Gets the year in this years range that the given Date falls on.\n * Returns null if the given Date is not in this range.\n */\n private _getYearInCurrentRange(date: D) {\n const year = this._adapter.getYear(date);\n return this._isInRange(year) ? year : null;\n }\n\n /**\n * Validate if the current year is in the current range\n * Returns true if is in range else returns false\n */\n private _isInRange(year: number): boolean {\n return true;\n }\n\n /**\n * @param obj The object to check.\n * @returns The given object if it is both a date instance and valid, otherwise null.\n */\n private _getValidDateOrNull(obj: any): D | null {\n return this._adapter.isDateInstance(obj) && this._adapter.isValid(obj) ? obj : null;\n }\n}\n\nexport function isSameMultiYearView<D>(\n dateAdapter: DatetimeAdapter<D>,\n date1: D,\n date2: D,\n minDate: D | null,\n maxDate: D | null\n): boolean {\n const year1 = dateAdapter.getYear(date1);\n const year2 = dateAdapter.getYear(date2);\n const startingYear = getStartingYear(dateAdapter, minDate, maxDate);\n return (\n Math.floor((year1 - startingYear) / yearsPerPage) ===\n Math.floor((year2 - startingYear) / yearsPerPage)\n );\n}\n\n/**\n * When the multi-year view is first opened, the active year will be in view.\n * So we compute how many years are between the active year and the *slot* where our\n * \"startingYear\" will render when paged into view.\n */\nexport function getActiveOffset<D>(\n dateAdapter: DatetimeAdapter<D>,\n activeDate: D,\n minDate: D | null,\n maxDate: D | null\n): number {\n const activeYear = dateAdapter.getYear(activeDate);\n return euclideanModulo(activeYear - getStartingYear(dateAdapter, minDate, maxDate), yearsPerPage);\n}\n\n/**\n * We pick a \"starting\" year such that either the maximum year would be at the end\n * or the minimum year would be at the beginning of a page.\n */\nfunction getStartingYear<D>(\n dateAdapter: DatetimeAdapter<D>,\n minDate: D | null,\n maxDate: D | null\n): number {\n let startingYear = 0;\n if (maxDate) {\n const maxYear = dateAdapter.getYear(maxDate);\n startingYear = maxYear - yearsPerPage + 1;\n } else if (minDate) {\n startingYear = dateAdapter.getYear(minDate);\n }\n return startingYear;\n}\n\n/** Gets remainder that is non-negative, even if first number is negative */\nfunction euclideanModulo(a: number, b: number): number {\n return ((a % b) + b) % b;\n}\n","<table class=\"mtx-calendar-table\" role=\"grid\">\n <thead class=\"mtx-calendar-table-header\"></thead>\n <tbody mtx-calendar-body\n (@slideCalendar.done)=\"_calendarStateDone()\"\n [@slideCalendar]=\"_calendarState\"\n [todayValue]=\"_todayYear\"\n [rows]=\"_years\"\n [numCols]=\"4\"\n [activeCell]=\"_getActiveCell()\"\n [allowDisabledSelection]=\"true\"\n [selectedValue]=\"_selectedYear!\"\n (selectedValueChange)=\"_yearSelected($event)\"></tbody>\n</table>\n","import { Injectable } from '@angular/core';\nimport { Subject } from 'rxjs';\n\n@Injectable({ providedIn: 'root' })\nexport class MtxDatetimepickerIntl {\n /**\n * Stream to emit from when labels are changed. Use this to notify components when the labels have\n * changed after initialization.\n */\n readonly changes = new Subject<void>();\n\n /** A label for the calendar popup (used by screen readers). */\n calendarLabel = 'Calendar';\n\n /** A label for the button used to open the calendar popup (used by screen readers). */\n openCalendarLabel = 'Open calendar';\n\n /** Label for the button used to close the calendar popup. */\n closeCalendarLabel = 'Close calendar';\n\n /** A label for the previous month button (used by screen readers). */\n prevMonthLabel = 'Previous month';\n\n /** A label for the next month button (used by screen readers). */\n nextMonthLabel = 'Next month';\n\n /** A label for the previous year button (used by screen readers). */\n prevYearLabel = 'Previous year';\n\n /** A label for the next year button (used by screen readers). */\n nextYearLabel = 'Next year';\n\n /** A label for the previous multi-year button (used by screen readers). */\n prevMultiYearLabel = 'Previous 24 years';\n\n /** A label for the next multi-year button (used by screen readers). */\n nextMultiYearLabel = 'Next 24 years';\n\n /** A label for the 'switch to month view' button (used by screen readers). */\n switchToMonthViewLabel = 'Choose date';\n\n /** A label for the 'switch to year view' button (used by screen readers). */\n switchToYearViewLabel = 'Choose month';\n\n /** A label for the 'switch to multi-year view' button (used by screen readers). */\n switchToMultiYearViewLabel = 'Choose month and year';\n\n /** A label for the first date of a range of dates (used by screen readers). */\n startDateLabel = 'Start date';\n\n /** A label for the last date of a range of dates (used by screen readers). */\n endDateLabel = 'End date';\n\n /** Formats a range of years (used for visuals). */\n formatYearRange(start: string, end: string): string {\n return `${start} \\u2013 ${end}`;\n }\n\n /** Formats a label for a range of years (used by screen readers). */\n formatYearRangeLabel(start: string, end: string): string {\n return `${start} to ${end}`;\n }\n\n /** A label for the 'switch to clock hour view' button (used by screen readers). */\n switchToClockHourViewLabel = 'Choose hour';\n\n /** A label for the 'switch to clock minute view' button (used by screen readers). */\n switchToClockMinuteViewLabel = 'Choose minute';\n\n /** Label used for ok button within the manual time input. */\n okLabel = 'OK';\n\n /** Label used for cancel button within the manual time input. */\n cancelLabel = 'Cancel';\n}\n","import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';\nimport { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport {\n AfterViewInit,\n booleanAttribute,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n Directive,\n ElementRef,\n EventEmitter,\n Input,\n OnChanges,\n OnDestroy,\n Output,\n SimpleChanges,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { MatButton } from '@angular/material/button';\nimport { SubscriptionLike } from 'rxjs';\n\nimport { DatetimeAdapter } from '@ng-matero/extensions/core';\nimport { MtxClock, MtxClockView } from './clock';\nimport { MtxDatetimepickerFilterType } from './datetimepicker-filtertype';\nimport { MtxDatetimepickerIntl } from './datetimepicker-intl';\nimport { MtxAMPM } from './datetimepicker-types';\n\nfunction pad(num: NumberInput, size: number) {\n num = String(num);\n while (num.length < size) num = '0' + num;\n return num;\n}\n\n@Directive({\n selector: 'input.mtx-time-input',\n host: {\n '(blur)': 'blur($event)',\n '(focus)': 'focus($event)',\n },\n exportAs: 'mtxTimeInput',\n standalone: true,\n})\nexport class MtxTimeInput implements OnDestroy {\n @Input('timeInterval')\n set timeInterval(value: NumberInput) {\n this._interval = coerceNumberProperty(value);\n }\n private _interval: number = 1;\n\n @Input('timeMin')\n set timeMin(value: NumberInput) {\n this._min = coerceNumberProperty(value);\n }\n private _min = 0;\n\n @Input('timeMax')\n set timeMax(value: NumberInput) {\n this._max = coerceNumberProperty(value);\n }\n private _max = Infinity;\n\n @Input('timeValue')\n set timeValue(value: NumberInput) {\n this._value = coerceNumberProperty(value);\n if (!this.hasFocus) {\n this.writeValue(this._value);\n }\n this.writePlaceholder(this._value);\n }\n\n @Output() timeValueChanged = new EventEmitter<NumberInput>();\n\n private _value: NumberInput;\n\n private keyDownListener = this.keyDownHandler.bind(this);\n private keyPressListener = this.keyPressHandler.bind(this);\n private inputEventListener = this.inputChangedHandler.bind(this);\n\n constructor(\n private element: ElementRef,\n private cdr: ChangeDetectorRef\n ) {\n this.inputElement.addEventListener('keydown', this.keyDownListener, {\n passive: true,\n });\n\n // Do not passive since we want to be able to preventDefault()\n this.inputElement.addEventListener('keypress', this.keyPressListener);\n this.inputElement.addEventListener('input', this.inputEventListener, {\n passive: true,\n });\n }\n\n get hasFocus() {\n return this.element.nativeElement && this.element?.nativeElement === document?.activeElement;\n }\n\n get inputElement() {\n return this.element.nativeElement as HTMLInputElement;\n }\n\n // We look here at the placeholder value, because we write '' into the value on focus\n // placeholder should always be up to date with \"currentValue\"\n get valid() {\n // At the start _value is undefined therefore this would result in not valid and\n // make a ugly warning border afterwards we can safely check\n if (this._value) {\n const currentValue = String(this.inputElement.value);\n\n // It can be that currentValue is empty due to we removing the value on focus,\n // if that is the case we should check previous value which should be in the placeholder\n if (currentValue.length) {\n return this._value == this.inputElement.value;\n } else {\n return this._value == this.inputElement.placeholder;\n }\n }\n return true;\n }\n\n get invalid() {\n return !this.valid;\n }\n\n blur() {\n this.writeValue(this._value);\n this.writePlaceholder(this._value);\n }\n\n focus() {\n this.writeValue('');\n }\n\n /**\n * Write value to inputElement\n * @param value NumberInput\n */\n writeValue(value: NumberInput) {\n if (value !== '') {\n this.inputElement.value = pad(value, 2);\n } else {\n this.inputElement.value = '';\n }\n this.cdr.markForCheck();\n }\n\n /**\n * Writes value to placeholder\n * @param value NumberInput\n */\n writePlaceholder(value: NumberInput) {\n this.inputElement.placeholder = pad(value, 2);\n this.cdr.markForCheck();\n }\n\n keyDownHandler(event: KeyboardEvent) {\n if (String(this.inputElement.value).length > 0) {\n let value: number | null = null;\n if (event.keyCode === UP_ARROW) {\n value = coerceNumberProperty(this._value);\n value += this._interval;\n event.stopPropagation();\n } else if (event.keyCode === DOWN_ARROW) {\n value = coerceNumberProperty(this._value);\n value -= this._interval;\n event.stopPropagation();\n }\n\n // if value has changed\n if (typeof value === 'number') {\n this.writeValue(value);\n this.writePlaceholder(value);\n this.clampInputValue();\n this.timeValueChanged.emit(this._value);\n }\n }\n }\n\n /**\n * Prevent non number inputs in the inputElement with t