UNPKG

@progress/kendo-angular-dateinputs

Version:

Kendo UI for Angular Date Inputs Package - Everything you need to add date selection functionality to apps (DatePicker, TimePicker, DateInput, DateRangePicker, DateTimePicker, Calendar, and MultiViewCalendar).

729 lines (718 loc) 28.9 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, ChangeDetectorRef, ChangeDetectionStrategy, ElementRef, EventEmitter, HostBinding, Input, Output, NgZone, ViewChild, ViewChildren, QueryList, Optional, Renderer2 } from '@angular/core'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { IntlService } from '@progress/kendo-angular-intl'; import { cloneDate, getDate } from '@progress/kendo-date-math'; import { Keys, EventsOutsideAngularDirective } from '@progress/kendo-angular-common'; import { MIDNIGHT_DATE, MIN_TIME, MAX_TIME } from '../defaults'; import { TimeListComponent } from './timelist.component'; import { TimePickerDOMService } from './services/dom.service'; import { getNow, hasChange, isInTimeRange, timeInRange } from '../util'; import { generateGetters, generateSnappers, snapTime, valueMerger } from './util'; import { PickerService } from '../common/picker.service'; import { closest } from '../common/dom-queries'; import { currentFocusTarget, isPresent } from '../common/utils'; import { NgIf, NgFor } from '@angular/common'; import { TimeSelectorLocalizedMessagesDirective } from './localization/timeselector-localized-messages.directive'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; import * as i2 from "@progress/kendo-angular-intl"; import * as i3 from "./services/dom.service"; import * as i4 from "../common/picker.service"; const listReducer = (state, list, idx, all) => { if (state.length || !list.isActive) { return state; } return [{ next: all[idx + 1] || list, prev: all[idx - 1] || list }]; }; var Direction; (function (Direction) { Direction[Direction["Left"] = 0] = "Left"; Direction[Direction["Right"] = 1] = "Right"; })(Direction || (Direction = {})); /** * @hidden * * Represents the Kendo UI TimeSelector component for Angular. */ export class TimeSelectorComponent { localization; cdr; element; intl; dom; zone; renderer; pickerService; accept; cancel; now; timeLists; timeListWrappers; /** * @hidden */ get disabledClass() { return this.disabled; } /** * Specifies the time format used to display the time list columns. */ format = 't'; /** * Specifies the smallest valid time value. */ min = cloneDate(MIN_TIME); /** * Specifies the biggest valid time value. */ max = cloneDate(MAX_TIME); /** * Determines whether to display the **Cancel** button in the popup. */ cancelButton = true; /** * Determines whether to display the **Set** button in the popup. */ setButton = true; /** * Determines whether to display the **Now** button in the popup. * * > If the current time is out of range or the incremental step is greater than `1`, the **Now** button will be hidden. */ nowButton = true; /** * Sets or gets the `disabled` property of the TimeSelector and determines whether the component is active. */ disabled = false; /** * Needed in order to set it in the dom service so that timeselector height can be properly calculated */ isAdaptiveEnabled; isDateTimePicker; /** * Configures the incremental steps of the TimeSelector. * * The available options are: * - `hour: Number`&mdash;Controls the incremental step of the hour value. * - `minute: Number`&mdash;Controls the incremental step of the minute value. * - `second: Number`&mdash;Controls the incremental step of the second value. * * @example * ```ts * _@Component({ * selector: 'my-app', * template: ` * <kendo-timeselector format="HH:mm:ss" [steps]="steps"></kendo-timeselector> * ` * }) * export class AppComponent { * public steps = { hour: 2, minute: 15, second: 15 }; * } * ``` * * > If the incremental step is greater than `1`, the **Now** button will be hidden. */ set steps(steps) { this._steps = steps || {}; } get steps() { return this._steps; } /** * Specifies the value of the TimeSelector component. */ value = null; /** * Fires each time the user selects a new value. */ valueChange = new EventEmitter(); /** * Fires each time the user cancels the selected value. */ valueReject = new EventEmitter(); tabOutLastPart = new EventEmitter(); tabOutFirstPart = new EventEmitter(); tabOutNow = new EventEmitter(); dateFormatParts; isActive = false; showNowButton = true; set current(value) { this._current = timeInRange(this.snapTime(cloneDate(value || MIDNIGHT_DATE), this.min), this.min, this.max); if (!NgZone.isInAngularZone()) { this.cdr.detectChanges(); } } get current() { return this._current; } get activeListIndex() { return this._activeListIndex; } set activeListIndex(value) { this._activeListIndex = value; if (!this.timeListWrappers || !this.timeListWrappers.length) { return; } this.timeListWrappers.forEach(listWrapper => { this.renderer.removeClass(listWrapper.nativeElement, 'k-focus'); }); if (value >= 0) { const listIndex = this.listIndex(value); const focusedWrapper = this.timeListWrappers.toArray()[listIndex]; if (focusedWrapper) { this.renderer.addClass(focusedWrapper.nativeElement, 'k-focus'); } } } mergeValue; snapTime; _activeListIndex = -1; _current; _steps = {}; subscriptions; domEvents = []; constructor(localization, cdr, element, intl, dom, zone, renderer, pickerService) { this.localization = localization; this.cdr = cdr; this.element = element; this.intl = intl; this.dom = dom; this.zone = zone; this.renderer = renderer; this.pickerService = pickerService; if (this.pickerService) { this.pickerService.timeSelector = this; } } /** * @hidden */ ngOnInit() { this.subscriptions = this.intl.changes.subscribe(this.intlChange.bind(this)); if (this.localization) { this.subscriptions.add(this.localization .changes .subscribe(() => this.cdr.markForCheck())); } this.renderer.addClass(this.element.nativeElement, 'k-timeselector'); this.dom.isAdaptiveEnabled = this.isAdaptiveEnabled; this.dom.isDateTimePicker = this.isDateTimePicker; this.dom.calculateHeights(this.element.nativeElement); this.init(); this.bindEvents(); } /** * @hidden */ ngOnChanges() { this.init(); } ngOnDestroy() { if (this.subscriptions) { this.subscriptions.unsubscribe(); } if (this.pickerService) { this.pickerService.timeSelector = null; } this.domEvents.forEach(unbindCallback => unbindCallback()); } /** * Focuses the TimeSelector component. * * @example * ```ts * _@Component({ * selector: 'my-app', * template: ` * <button (click)="timeselector.focus()">Focus time picker</button> * <kendo-timeselector #timeselector></kendo-timeselector> * ` * }) * export class AppComponent { } * ``` */ focus() { const list = this.timeLists.first; if (!list) { return; } list.focus(); } /** * Blurs the TimeSelector component. */ blur() { const list = this.timeLists.first; if (!list) { return; } list.blur(); } /** * @hidden */ handleAccept() { this.handleChange(this.mergeValue(cloneDate(this.value || getDate(getNow())), this.current)); } /** * @hidden */ handleNow() { this.current = getNow(); this.handleChange(this.current); this.cdr.markForCheck(); } /** * @hidden */ handleReject() { this.current = this.value; this.valueReject.emit(); } /** * @hidden */ handleFocus(args) { if (this.isActive) { return; } this.isActive = true; this.emitFocus(args); } /** * @hidden */ handleListFocus(args) { const index = parseInt(args.target.getAttribute('data-timelist-index'), 10); this.activeListIndex = index; this.handleFocus(args); } /** * @hidden */ handleBlur(args) { const currentTarget = currentFocusTarget(args); if (currentTarget && this.containsElement(currentTarget)) { return; } this.activeListIndex = -1; this.isActive = false; this.emitBlur(args); } /** * @hidden */ containsElement(element) { return Boolean(closest(element, node => node === this.element.nativeElement)); } /** * @hidden */ handleTabOut(event) { const { keyCode, shiftKey } = event; if (event.target === this.now?.nativeElement && keyCode === Keys.Tab && shiftKey) { event.preventDefault(); if (this.isDateTimePicker) { this.tabOutNow.emit(); } else { this.cancel ? this.cancel.nativeElement.focus() : this.accept?.nativeElement.focus(); } return; } if (keyCode === Keys.Tab && !shiftKey && event.target !== this.now?.nativeElement) { event.preventDefault(); if (document.activeElement === this.accept.nativeElement) { if (this.cancel) { this.cancel.nativeElement.focus(); } else { this.now ? this.now.nativeElement.focus() : this.timeLists.first.focus(); } } else { this.now ? this.now.nativeElement.focus() : this.timeLists.first.focus(); } } } partStep(part) { return this.steps[part.type] || 1; } init(changes) { if (!changes || hasChange(changes, 'format')) { this.dateFormatParts = this.intl.splitDateFormat(this.format); this.mergeValue = valueMerger(generateGetters(this.dateFormatParts)); } if (!changes || hasChange(changes, 'steps')) { this.snapTime = snapTime(generateSnappers(this.steps)); } if (!changes || hasChange(changes, 'value')) { this.current = this.value; } this.showNowButton = !this.hasSteps() && this.nowButton && isInTimeRange(getNow(), this.min, this.max); } focusList(dir) { if (!this.timeLists.length) { return; } this.timeLists.reduce(listReducer, []) .map(state => dir === Direction.Right ? state.next : state.prev) .map(list => list && list.focus()); } handleChange(value) { this.value = value; this.valueChange.emit(cloneDate(value)); } hasActiveButton() { if (!this.accept) { return false; } return [this.accept, this.cancel, this.now].reduce((isActive, el) => isActive || this.dom.isActive(el), false); } hasSteps() { const keys = Object.keys(this.steps); return keys.length !== keys.reduce((acc, k) => acc + this.steps[k], 0); } intlChange() { this.dateFormatParts = this.intl.splitDateFormat(this.format); this.mergeValue = valueMerger(generateGetters(this.dateFormatParts)); this.cdr.markForCheck(); } bindEvents() { if (this.element) { this.zone.runOutsideAngular(() => { this.domEvents.push(this.renderer.listen(this.element.nativeElement, 'keydown', this.handleKeydown.bind(this))); }); } } handleKeydown(args) { const { keyCode, altKey } = args; // reserve the alt + arrow key commands for the picker const arrowKeyPressed = [Keys.ArrowLeft, Keys.ArrowRight].indexOf(keyCode) !== -1; if (isPresent(this.pickerService) && arrowKeyPressed && altKey) { return; } if (keyCode === Keys.Enter && !this.hasActiveButton()) { this.handleAccept(); } else if (keyCode === Keys.ArrowLeft || keyCode === Keys.ArrowRight) { this.focusList(keyCode === Keys.ArrowLeft ? Direction.Left : Direction.Right); } } emitBlur(args) { if (this.pickerService) { this.pickerService.onBlur.emit(args); } } emitFocus(args) { if (this.pickerService) { this.pickerService.onFocus.emit(args); } } listIndex(partIndex) { let listIdx = 0; let partIdx = 0; while (partIdx < partIndex) { if (this.dateFormatParts[partIdx].type !== 'literal') { listIdx++; } partIdx++; } return listIdx; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TimeSelectorComponent, deps: [{ token: i1.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i2.IntlService }, { token: i3.TimePickerDOMService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i4.PickerService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TimeSelectorComponent, isStandalone: true, selector: "kendo-timeselector", inputs: { format: "format", min: "min", max: "max", cancelButton: "cancelButton", setButton: "setButton", nowButton: "nowButton", disabled: "disabled", isAdaptiveEnabled: "isAdaptiveEnabled", isDateTimePicker: "isDateTimePicker", steps: "steps", value: "value" }, outputs: { valueChange: "valueChange", valueReject: "valueReject", tabOutLastPart: "tabOutLastPart", tabOutFirstPart: "tabOutFirstPart", tabOutNow: "tabOutNow" }, host: { properties: { "class.k-disabled": "this.disabledClass" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.timeselector' } ], viewQueries: [{ propertyName: "accept", first: true, predicate: ["accept"], descendants: true }, { propertyName: "cancel", first: true, predicate: ["cancel"], descendants: true }, { propertyName: "now", first: true, predicate: ["now"], descendants: true }, { propertyName: "timeLists", predicate: TimeListComponent, descendants: true }, { propertyName: "timeListWrappers", predicate: ["listWrapper"], descendants: true }], exportAs: ["kendo-timeselector"], usesOnChanges: true, ngImport: i0, template: ` <ng-container kendoTimeSelectorLocalizedMessages i18n-accept="kendo.timeselector.accept|The Accept button text in the timeselector component" accept="Set" i18n-acceptLabel="kendo.timeselector.acceptLabel|The label for the Accept button in the timeselector component" acceptLabel="Set time" i18n-cancel="kendo.timeselector.cancel|The Cancel button text in the timeselector component" cancel="Cancel" i18n-cancelLabel="kendo.timeselector.cancelLabel|The label for the Cancel button in the timeselector component" cancelLabel="Cancel changes" i18n-now="kendo.timeselector.now|The Now button text in the timeselector component" now="Now" i18n-nowLabel="kendo.timeselector.nowLabel|The label for the Now button in the timeselector component" nowLabel="Select now" > </ng-container> <div class="k-time-header"> <span class="k-title k-timeselector-title"> {{ intl.formatDate(current, format) }} </span> <button #now *ngIf="showNowButton" type="button" class="k-button k-button-md k-rounded-md k-button-flat k-button-flat-primary k-time-now" [attr.title]="localization.get('nowLabel')" [attr.aria-label]="localization.get('nowLabel')" [kendoEventsOutsideAngular]="{ click: handleNow, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('now')}}</button> </div> <div class="k-time-list-container"> <span class="k-time-highlight"></span> <ng-template ngFor [ngForOf]="dateFormatParts" let-part let-idx="index"> <div #listWrapper class="k-time-list-wrapper" role="presentation" tabindex="-1" *ngIf="part.type !== 'literal'" > <span class="k-title k-timeselector-title">{{intl.dateFieldName(part)}}</span> <kendo-timelist [isLast]="idx === dateFormatParts.length - 1" [isFirst]="idx === 0" [min]="min" [max]="max" [part]="part" [step]="partStep(part)" [disabled]="disabled" [(value)]="current" (tabOutLastPart)="tabOutLastPart.emit()" (tabOutFirstPart)="tabOutFirstPart.emit()" [kendoEventsOutsideAngular]="{ focus: handleListFocus, blur: handleBlur }" [scope]="this" [attr.data-timelist-index]="idx" ></kendo-timelist> </div> <div class="k-time-separator" *ngIf="part.type === 'literal'"> {{part.pattern}} </div> </ng-template> </div> <div class="k-time-footer k-actions k-actions-stretched k-actions-horizontal" *ngIf="setButton || cancelButton"> <button #accept *ngIf="setButton" type="button" class="k-button k-time-accept k-button-md k-rounded-md k-button-solid k-button-solid-primary" [attr.title]="localization.get('acceptLabel')" [attr.aria-label]="localization.get('acceptLabel')" [kendoEventsOutsideAngular]="{ click: handleAccept, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('accept')}}</button> <button #cancel *ngIf="cancelButton" class="k-button k-time-cancel k-button-md k-rounded-md k-button-solid k-button-solid-base" type="button" [attr.title]="localization.get('cancelLabel')" [attr.aria-label]="localization.get('cancelLabel')" [kendoEventsOutsideAngular]="{ click: handleReject, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('cancel')}}</button> </div> `, isInline: true, dependencies: [{ kind: "directive", type: TimeSelectorLocalizedMessagesDirective, selector: "[kendoTimeSelectorLocalizedMessages]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: TimeListComponent, selector: "kendo-timelist", inputs: ["min", "max", "part", "step", "disabled", "value", "isLast", "isFirst"], outputs: ["valueChange", "tabOutLastPart", "tabOutFirstPart"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TimeSelectorComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'kendo-timeselector', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.timeselector' } ], selector: 'kendo-timeselector', template: ` <ng-container kendoTimeSelectorLocalizedMessages i18n-accept="kendo.timeselector.accept|The Accept button text in the timeselector component" accept="Set" i18n-acceptLabel="kendo.timeselector.acceptLabel|The label for the Accept button in the timeselector component" acceptLabel="Set time" i18n-cancel="kendo.timeselector.cancel|The Cancel button text in the timeselector component" cancel="Cancel" i18n-cancelLabel="kendo.timeselector.cancelLabel|The label for the Cancel button in the timeselector component" cancelLabel="Cancel changes" i18n-now="kendo.timeselector.now|The Now button text in the timeselector component" now="Now" i18n-nowLabel="kendo.timeselector.nowLabel|The label for the Now button in the timeselector component" nowLabel="Select now" > </ng-container> <div class="k-time-header"> <span class="k-title k-timeselector-title"> {{ intl.formatDate(current, format) }} </span> <button #now *ngIf="showNowButton" type="button" class="k-button k-button-md k-rounded-md k-button-flat k-button-flat-primary k-time-now" [attr.title]="localization.get('nowLabel')" [attr.aria-label]="localization.get('nowLabel')" [kendoEventsOutsideAngular]="{ click: handleNow, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('now')}}</button> </div> <div class="k-time-list-container"> <span class="k-time-highlight"></span> <ng-template ngFor [ngForOf]="dateFormatParts" let-part let-idx="index"> <div #listWrapper class="k-time-list-wrapper" role="presentation" tabindex="-1" *ngIf="part.type !== 'literal'" > <span class="k-title k-timeselector-title">{{intl.dateFieldName(part)}}</span> <kendo-timelist [isLast]="idx === dateFormatParts.length - 1" [isFirst]="idx === 0" [min]="min" [max]="max" [part]="part" [step]="partStep(part)" [disabled]="disabled" [(value)]="current" (tabOutLastPart)="tabOutLastPart.emit()" (tabOutFirstPart)="tabOutFirstPart.emit()" [kendoEventsOutsideAngular]="{ focus: handleListFocus, blur: handleBlur }" [scope]="this" [attr.data-timelist-index]="idx" ></kendo-timelist> </div> <div class="k-time-separator" *ngIf="part.type === 'literal'"> {{part.pattern}} </div> </ng-template> </div> <div class="k-time-footer k-actions k-actions-stretched k-actions-horizontal" *ngIf="setButton || cancelButton"> <button #accept *ngIf="setButton" type="button" class="k-button k-time-accept k-button-md k-rounded-md k-button-solid k-button-solid-primary" [attr.title]="localization.get('acceptLabel')" [attr.aria-label]="localization.get('acceptLabel')" [kendoEventsOutsideAngular]="{ click: handleAccept, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('accept')}}</button> <button #cancel *ngIf="cancelButton" class="k-button k-time-cancel k-button-md k-rounded-md k-button-solid k-button-solid-base" type="button" [attr.title]="localization.get('cancelLabel')" [attr.aria-label]="localization.get('cancelLabel')" [kendoEventsOutsideAngular]="{ click: handleReject, focus: handleFocus, blur: handleBlur, keydown: handleTabOut }" [scope]="this" [disabled]="disabled" >{{localization.get('cancel')}}</button> </div> `, standalone: true, imports: [TimeSelectorLocalizedMessagesDirective, NgIf, EventsOutsideAngularDirective, NgFor, TimeListComponent] }] }], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i2.IntlService }, { type: i3.TimePickerDOMService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i4.PickerService, decorators: [{ type: Optional }] }]; }, propDecorators: { accept: [{ type: ViewChild, args: ['accept', { static: false }] }], cancel: [{ type: ViewChild, args: ['cancel', { static: false }] }], now: [{ type: ViewChild, args: ['now', { static: false }] }], timeLists: [{ type: ViewChildren, args: [TimeListComponent] }], timeListWrappers: [{ type: ViewChildren, args: ['listWrapper'] }], disabledClass: [{ type: HostBinding, args: ['class.k-disabled'] }], format: [{ type: Input }], min: [{ type: Input }], max: [{ type: Input }], cancelButton: [{ type: Input }], setButton: [{ type: Input }], nowButton: [{ type: Input }], disabled: [{ type: Input }], isAdaptiveEnabled: [{ type: Input }], isDateTimePicker: [{ type: Input }], steps: [{ type: Input }], value: [{ type: Input }], valueChange: [{ type: Output }], valueReject: [{ type: Output }], tabOutLastPart: [{ type: Output }], tabOutFirstPart: [{ type: Output }], tabOutNow: [{ type: Output }] } });