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).

384 lines (383 loc) 15.9 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ /* eslint-disable @angular-eslint/component-selector */ import { Component, EventEmitter, ElementRef, HostBinding, ViewChild, Renderer2, NgZone, Injector, Input, Output } from '@angular/core'; import { debounceTime, map } from 'rxjs/operators'; import { cloneDate } from '@progress/kendo-date-math'; import { VirtualizationComponent } from '../virtualization/virtualization.component'; import { MAX_TIME, MIDNIGHT_DATE } from '../defaults'; import { TIME_PART } from './models/time-part.default'; import { TimePickerDOMService } from './services/dom.service'; import { HoursService } from './services/hours.service'; import { MinutesService } from './services/minutes.service'; import { SecondsService } from './services/seconds.service'; import { MillisecondsService } from './services/milliseconds.service'; import { DayPeriodService } from './services/dayperiod.service'; import { closestInScope } from '../common/dom-queries'; import { LocalizationService } from '@progress/kendo-angular-l10n'; import { Keys, EventsOutsideAngularDirective } from '@progress/kendo-angular-common'; import { NgStyle, NgFor } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "./services/dom.service"; import * as i2 from "@progress/kendo-angular-l10n"; const SNAP_THRESHOLD = 0.05; //% of the item height const SCROLL_THRESHOLD = 2; //< 2px threshold const nil = () => (null); const getters = { 35: (data, _) => data[data.length - 1], 36: (data, _) => data[0], 38: (data, index) => data[index - 1], 40: (data, index) => data[index + 1] }; const services = { [TIME_PART.dayperiod]: DayPeriodService, [TIME_PART.hour]: HoursService, [TIME_PART.minute]: MinutesService, [TIME_PART.second]: SecondsService, [TIME_PART.millisecond]: MillisecondsService }; /** * @hidden */ export class TimeListComponent { element; injector; dom; renderer; zone; localization; min = cloneDate(MIDNIGHT_DATE); max = cloneDate(MAX_TIME); part; step = 1; disabled = false; value; isLast = false; isFirst = false; valueChange = new EventEmitter(); tabOutLastPart = new EventEmitter(); tabOutFirstPart = new EventEmitter(); virtualization; get tabIndex() { return this.disabled ? undefined : 0; } componentClass = true; get isDayPeriod() { return this.part?.type === 'dayperiod'; } get currentSelectedIndex() { return this.selectedIndex(this.value); } get roleAttribute() { return 'listbox'; } get ariaLabel() { return this.localization.get(this.part?.type); } animateToIndex = true; isActive = false; skip = 0; total = 60; service; itemHeight; listHeight; topOffset; bottomOffset; bottomThreshold; topThreshold; style; data = []; indexToScroll = -1; scrollSubscription; domEvents = []; constructor(element, injector, dom, renderer, zone, localization) { this.element = element; this.injector = injector; this.dom = dom; this.renderer = renderer; this.zone = zone; this.localization = localization; } ngOnChanges(changes) { if (changes.part) { this.service = this.injector.get(services[this.part.type]); this.service.configure(this.serviceSettings()); } const value = this.value; const valueChanges = changes.value || {}; const [min, max] = this.service.limitRange(this.min, this.max, value); if (this.service.isRangeChanged(min, max) || changes.min || changes.max || changes.step) { this.data = []; this.service.configure(this.serviceSettings({ min, max })); } // Skip the rendering of the list whenever possible if (!this.data.length || this.hasMissingValue(valueChanges)) { this.animateToIndex = false; this.data = this.service.data(value); } this.animateToIndex = this.animateToIndex && this.textHasChanged(valueChanges); this.total = this.service.total(value); this.indexToScroll = this.selectedIndex(value); } ngOnInit() { this.animateToIndex = true; this.dom.ensureHeights(); this.itemHeight = this.dom.itemHeight; this.listHeight = this.dom.timeListHeight; this.topOffset = (this.listHeight - this.itemHeight) / 2; this.bottomOffset = this.listHeight - this.itemHeight; this.topThreshold = this.itemHeight * SNAP_THRESHOLD; this.bottomThreshold = this.itemHeight * (1 - SNAP_THRESHOLD); const translate = `translateY(${this.topOffset}px)`; this.style = { transform: translate, '-ms-transform': translate }; if (this.element) { this.zone.runOutsideAngular(() => { this.bindEvents(); }); } } ngOnDestroy() { this.scrollSubscription.unsubscribe(); this.domEvents.forEach(unbindCallback => unbindCallback()); } ngAfterViewInit() { this.scrollOnce((index) => this.virtualization.scrollToIndex(index)); } ngAfterViewChecked() { this.scrollOnce((index) => { const action = this.animateToIndex ? 'animateToIndex' : 'scrollToIndex'; this.virtualization[action](index); this.animateToIndex = true; }); } getCurrentItem() { return this.indexToScroll >= 0 ? this.data[this.indexToScroll] : null; } handleChange(dataItem) { const candidate = this.service.apply(this.value, dataItem.value); if (this.value.getTime() === candidate.getTime()) { return; } this.indexToScroll = this.data.indexOf(dataItem); this.value = candidate; this.valueChange.emit(candidate); } handleItemClick(args) { const item = closestInScope(args.target, node => node.hasAttribute('data-timelist-item-index'), this.element.nativeElement); if (item) { const index = item.getAttribute('data-timelist-item-index'); this.handleChange(this.data[index]); } } /** * Focuses the host element of the TimeList. * * @example * ```ts * _@Component({ * selector: 'my-app', * template: ` * <button (click)="timelist.focus()">Focus TimeList</button> * <kendo-timelist #timelist></kendo-timelist> * ` * }) * export class AppComponent { } * ``` */ focus() { if (!this.element) { return; } this.element.nativeElement.focus(); } /** * Blurs the TimeList component. */ blur() { if (!this.element) { return; } this.element.nativeElement.blur(); } itemOffset(scrollTop) { const valueIndex = this.selectedIndex(this.value); const activeIndex = this.virtualization.activeIndex(); const offset = this.virtualization.itemOffset(activeIndex); const distance = Math.abs(Math.ceil(scrollTop) - offset); if (valueIndex === activeIndex && distance < SCROLL_THRESHOLD) { return offset; } const scrollUp = valueIndex > activeIndex; const moveToNext = scrollUp && distance >= this.bottomThreshold || !scrollUp && distance > this.topThreshold; return moveToNext ? this.virtualization.itemOffset(activeIndex + 1) : offset; } hasMissingValue({ previousValue, currentValue }) { const isPreviousMissing = previousValue && !this.service.valueInList(previousValue); const isCurrentMissing = currentValue && !this.service.valueInList(currentValue); return isPreviousMissing || isCurrentMissing; } scrollOnce(action) { if (this.indexToScroll !== -1) { action(this.indexToScroll); this.indexToScroll = -1; } } serviceSettings(settings) { const defaults = { boundRange: false, insertUndividedMax: false, max: this.max, min: this.min, part: this.part, step: this.step }; const result = Object.assign({}, defaults, settings); result.boundRange = result.part.type !== 'hour'; return result; } selectedIndex(value) { if (!value) { return -1; } return this.service.selectedIndex(value); } textHasChanged({ previousValue, currentValue }) { if (!previousValue || !currentValue) { return false; } const oldData = this.data[this.selectedIndex(previousValue)]; const newData = this.data[this.selectedIndex(currentValue)]; return oldData && newData && oldData.text !== newData.text; } handleKeyDown(e) { if (e.keyCode === Keys.Tab && !e.shiftKey && this.isLast) { e.preventDefault(); this.tabOutLastPart.emit(); } if (e.keyCode === Keys.Tab && e.shiftKey && this.isFirst) { e.preventDefault(); this.tabOutFirstPart.emit(); } const getter = getters[e.keyCode] || nil; const dataItem = getter(this.data, this.service.selectedIndex(this.value)); if (dataItem) { this.handleChange(dataItem); e.preventDefault(); } } bindEvents() { this.scrollSubscription = this.virtualization .scroll$() .pipe(debounceTime(100), map((e) => e.target.scrollTop), map((top) => this.itemOffset(top)), map((itemOffset) => this.virtualization.itemIndex(itemOffset))) .subscribe(index => { this.virtualization.scrollToIndex(index); this.handleChange(this.data[index]); }); const element = this.element.nativeElement; this.domEvents.push(this.renderer.listen(element, 'mouseover', () => !this.isActive && this.focus()), this.renderer.listen(element, 'click', () => this.focus()), this.renderer.listen(element, 'blur', () => this.isActive = false), this.renderer.listen(element, 'focus', () => this.isActive = true), this.renderer.listen(element, 'keydown', this.handleKeyDown.bind(this))); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TimeListComponent, deps: [{ token: i0.ElementRef }, { token: i0.Injector }, { token: i1.TimePickerDOMService }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i2.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TimeListComponent, isStandalone: true, selector: "kendo-timelist", inputs: { min: "min", max: "max", part: "part", step: "step", disabled: "disabled", value: "value", isLast: "isLast", isFirst: "isFirst" }, outputs: { valueChange: "valueChange", tabOutLastPart: "tabOutLastPart", tabOutFirstPart: "tabOutFirstPart" }, host: { properties: { "attr.tabindex": "this.tabIndex", "class.k-time-list": "this.componentClass" } }, viewQueries: [{ propertyName: "virtualization", first: true, predicate: VirtualizationComponent, descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: ` <kendo-virtualization [attr.role]="roleAttribute" [attr.aria-label]="ariaLabel" [skip]="skip" [take]="total" [total]="total" [itemHeight]="itemHeight" [maxScrollDifference]="listHeight" [topOffset]="topOffset" [bottomOffset]="bottomOffset" class="k-time-container" tabindex="-1" > <ul [ngStyle]="style" class="k-reset" [kendoEventsOutsideAngular]="{ click: handleItemClick }" [scope]="this" [attr.role]="'presentation'" > <li *ngFor="let item of data; let index = index;" class="k-item" [attr.data-timelist-item-index]="index" [attr.role]="'option'" [attr.aria-selected]="index === currentSelectedIndex" > <span>{{item.text}}</span> </li> </ul> </kendo-virtualization> `, isInline: true, dependencies: [{ kind: "component", type: VirtualizationComponent, selector: "kendo-virtualization", inputs: ["direction", "itemHeight", "itemWidth", "topOffset", "bottomOffset", "maxScrollDifference", "scrollOffsetSize", "scrollDuration", "skip", "take", "total"], outputs: ["activeIndexChange", "pageChange", "scrollChange"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TimeListComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-timelist', template: ` <kendo-virtualization [attr.role]="roleAttribute" [attr.aria-label]="ariaLabel" [skip]="skip" [take]="total" [total]="total" [itemHeight]="itemHeight" [maxScrollDifference]="listHeight" [topOffset]="topOffset" [bottomOffset]="bottomOffset" class="k-time-container" tabindex="-1" > <ul [ngStyle]="style" class="k-reset" [kendoEventsOutsideAngular]="{ click: handleItemClick }" [scope]="this" [attr.role]="'presentation'" > <li *ngFor="let item of data; let index = index;" class="k-item" [attr.data-timelist-item-index]="index" [attr.role]="'option'" [attr.aria-selected]="index === currentSelectedIndex" > <span>{{item.text}}</span> </li> </ul> </kendo-virtualization> `, standalone: true, imports: [VirtualizationComponent, NgStyle, EventsOutsideAngularDirective, NgFor] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Injector }, { type: i1.TimePickerDOMService }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i2.LocalizationService }]; }, propDecorators: { min: [{ type: Input }], max: [{ type: Input }], part: [{ type: Input }], step: [{ type: Input }], disabled: [{ type: Input }], value: [{ type: Input }], isLast: [{ type: Input }], isFirst: [{ type: Input }], valueChange: [{ type: Output }], tabOutLastPart: [{ type: Output }], tabOutFirstPart: [{ type: Output }], virtualization: [{ type: ViewChild, args: [VirtualizationComponent, { static: true }] }], tabIndex: [{ type: HostBinding, args: ["attr.tabindex"] }], componentClass: [{ type: HostBinding, args: ["class.k-time-list"] }] } });