@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).
565 lines (558 loc) • 25.6 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, HostBinding, Input, Output, TemplateRef, ElementRef, Renderer2, HostListener } from '@angular/core';
import { cloneDate } from '@progress/kendo-date-math';
import { BusViewService } from './services/bus-view.service';
import { IntlService } from '@progress/kendo-angular-intl';
import { Action } from './models/navigation-action.enum';
import { CalendarViewEnum } from './models/view.enum';
import { MIN_DATE, MAX_DATE } from '../defaults';
import { dateInRange, hasChange, shiftWeekNames } from '../util';
import { isDocumentAvailable } from '@progress/kendo-angular-common';
import { attributeNames, isPresent } from '../common/utils';
import { KForOf } from './for.directive';
import { ViewComponent } from './view.component';
import { NgClass, NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "./services/bus-view.service";
import * as i2 from "@progress/kendo-angular-intl";
const DEFAULT_VIEWS_LENGTH = 2;
/**
* @hidden
*/
export class HorizontalViewListComponent {
bus;
intl;
cdr;
element;
renderer;
handleMultiViewCalendarFocus() {
this.focusCalendar.emit();
}
handleMultiViewCalendarBlur(event) {
this.blurCalendar.emit(event);
}
/**
* Needed for the MultiViewCalendar used in the Scheduler Year view
*/
showOtherMonthDays = true;
cellTemplateRef;
weekNumberTemplateRef;
allowReverse;
activeRangeEnd;
activeView = CalendarViewEnum.month;
cellUID;
focusedDate;
isActive = true;
min = new Date(MIN_DATE);
max = new Date(MAX_DATE);
selectionRange;
selectedDates = [];
views = DEFAULT_VIEWS_LENGTH;
showViewHeader = false;
animateNavigation = false;
orientation = 'horizontal';
activeDescendant;
tabIndex = 0;
disabled = false;
id;
weekDaysFormat = 'short';
get weekNumber() {
return this.showWeekNumbers && this.isMonthView();
}
set weekNumber(showWeekNumbers) {
this.showWeekNumbers = showWeekNumbers;
}
cellClick = new EventEmitter();
weekNumberCellClick = new EventEmitter();
cellEnter = new EventEmitter();
cellLeave = new EventEmitter();
activeDateChange = new EventEmitter();
focusCalendar = new EventEmitter();
blurCalendar = new EventEmitter();
focusedCellChange = new EventEmitter();
getComponentClass = true;
get horizontalHostClass() {
return this.orientation === 'horizontal';
}
get verticalHostClass() {
return this.orientation === 'vertical';
}
get getComponentMonthClass() {
return this.activeView === CalendarViewEnum.month;
}
get getComponentYearClass() {
return this.activeView === CalendarViewEnum.year;
}
get getComponentDecadeClass() {
return this.activeView === CalendarViewEnum.decade;
}
get getComponentCenturyClass() {
return this.activeView === CalendarViewEnum.century;
}
get role() {
return this.views >= 2 ? 'grid' : null;
}
get tabindex() {
return this.views >= 2 ? '0' : null;
}
get getActiveDescendant() {
return this.views === 1 ? this.activeDescendant : null;
}
get getTabIndex() {
return this.disabled || this.views >= 2 ? null : this.tabIndex;
}
service;
weekNames = [];
wideWeekNames = [];
activeDate;
dates = [];
skip;
total;
nextAnimationDate;
prevAnimationDate;
showWeekNumbers;
intlSubscription;
animation;
constructor(bus, intl, cdr, element, renderer) {
this.bus = bus;
this.intl = intl;
this.cdr = cdr;
this.element = element;
this.renderer = renderer;
this.intlSubscription = this.intl.changes.subscribe(this.intlChange.bind(this));
}
ngOnChanges(changes) {
this.initService();
if (this.weekNames.length === 0 || changes.weekNumber) {
this.weekNames = this.getWeekNames(this.weekDaysFormat);
this.wideWeekNames = this.getWeekNames('wide');
}
if (changes.weekDaysFormat && this.weekNames.length !== 0 && !changes.weekNumber) {
this.weekNames = this.getWeekNames(this.weekDaysFormat);
}
if (!this.service) {
return;
}
this.views = this.views || DEFAULT_VIEWS_LENGTH;
const focusedDate = this.focusedDate;
const viewDate = this.clampDate(this.service.viewDate(focusedDate, this.max, this.views));
this.skip = this.service.skip(viewDate, this.min);
this.total = this.service.total(this.min, this.max);
const activeViewChanged = hasChange(changes, 'activeView');
const viewsHasChanged = this.views > 0 && hasChange(changes, 'views');
if (activeViewChanged || !this.isInDates(focusedDate) || viewsHasChanged || !this.activeDate) {
this.dates = this.service.datesList(viewDate, this.getTake(this.skip));
this.activeDate = cloneDate(this.dates[0]);
this.activeDateChange.emit(this.activeDate);
}
this.setAriaActivedescendant();
//set tabindex for MultiViewCalendar
if (this.views >= 2) {
this.renderer.setAttribute(this.element.nativeElement, 'tabindex', this.tabIndex.toString());
}
}
ngOnDestroy() {
this.intlSubscription.unsubscribe();
}
initService() {
this.service = this.bus.service(this.activeView);
}
isMonthView() {
return this.activeView === CalendarViewEnum.month;
}
getCaptionTitle(date) {
return this.service.title(date);
}
handleClassicCalendarFocus() {
this.focusCalendar.emit();
}
handleClassicCalendarBlur(event) {
this.blurCalendar.emit(event);
}
animateView(action) {
const container = this.element.nativeElement;
const table = container.querySelector('table');
// the whole width excluding padding/margin
const initialContainerWidth = parseFloat(getComputedStyle(container).width);
// table width
const tableWidth = parseFloat(getComputedStyle(table).width);
this.renderer.setStyle(container, 'width', `${initialContainerWidth}px`);
this.renderer.setStyle(container, 'overflow', 'visible');
// initialize an additional view for the animation
if (action === Action.NextView) {
// animating Action.NextView requires adding an additional view before the rendered views
this.nextAnimationDate = cloneDate(this.dates[0]);
}
else {
// animating Action.PrevView requires adding an additional view after the rendered views
this.prevAnimationDate = cloneDate(this.dates[this.dates.length - 1]);
}
// run cdr to render the additional view
this.cdr.detectChanges();
container.querySelectorAll('table').forEach(table => {
this.renderer.setStyle(table, 'width', `${tableWidth}px`);
});
// we always slide by the width of 1 table
// cross-browser compatibility is ensured by measuring the client rectangle and substracting the gap
const tabRect = table.getBoundingClientRect();
const containerGap = parseFloat(getComputedStyle(container).columnGap);
const start = action === Action.NextView ? 'translateX(0)' : `translateX(-${tabRect.width + containerGap}px)`;
const end = action === Action.NextView ? `translateX(-${tabRect.width + containerGap}px)` : 'translateX(0)';
if (!this.animation) {
this.animation = container.animate([
{ transform: start },
{ transform: end }
], {
duration: 500,
easing: 'ease-out'
});
this.animation.oncancel = this.animation.onfinish = () => {
// clear all inline styles
this.renderer.removeStyle(container, 'width');
this.renderer.removeStyle(container, 'overflow');
container.querySelectorAll('table').forEach(table => {
this.renderer.removeStyle(table, 'width');
});
// clear the animation and the animation view
this.animation = null;
this.nextAnimationDate = null;
this.prevAnimationDate = null;
// run cdr to remove additional animation view from the markup
this.cdr.detectChanges();
};
}
else {
// if animation is already running, cancel it and show the end navigation result on multiple prev/next button clicks
this.animation.cancel();
}
}
navigate(action) {
if (this.animateNavigation && isDocumentAvailable() && isPresent(this.element.nativeElement.animate)) {
this.animateView(action);
}
const candidate = this.move(action);
const list = this.service.datesList(candidate, this.getTake(this.skip));
if (this.isListInRange(list)) {
this.dates = list;
}
this.activeDate = cloneDate(this.dates[0]);
this.focusedDate = cloneDate(candidate);
this.cdr.markForCheck();
this.activeDateChange.emit(this.activeDate);
return cloneDate(candidate);
}
canNavigate(action) {
if (!this.service) {
return false;
}
return this.isListInRange(this.service.datesList(this.move(action), this.getTake(this.skip)));
}
getWeekNames(type) {
const weekNames = shiftWeekNames(this.intl.dateFormatNames({ nameType: type, type: 'days' }), this.intl.firstDay());
return this.weekNumber ? [''].concat(weekNames) : weekNames;
}
intlChange() {
this.weekNames = this.getWeekNames(this.weekDaysFormat);
this.wideWeekNames = this.getWeekNames('wide');
this.cdr.markForCheck();
}
clampDate(value) {
return dateInRange(value, this.min, this.max);
}
move(action) {
return this.service.move(this.dates[0] || this.focusedDate, action);
}
isListInRange(list) {
const lowerBound = this.service.beginningOfPeriod(this.min);
const upperBound = this.service.beginningOfPeriod(this.service.addToDate(this.max, 1));
return lowerBound <= list[0] && list[list.length - 1] < upperBound;
}
isInDates(value) {
return this.service.isInArray(value, this.dates);
}
getTake(skip) {
return Math.min(this.total - skip, this.views);
}
setAriaActivedescendant() {
if (this.views >= 2) {
this.renderer.setAttribute(this.element.nativeElement, attributeNames.ariaActiveDescendant, this.activeDescendant);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HorizontalViewListComponent, deps: [{ token: i1.BusViewService }, { token: i2.IntlService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: HorizontalViewListComponent, isStandalone: true, selector: "kendo-calendar-horizontal", inputs: { showOtherMonthDays: "showOtherMonthDays", cellTemplateRef: "cellTemplateRef", weekNumberTemplateRef: "weekNumberTemplateRef", allowReverse: "allowReverse", activeRangeEnd: "activeRangeEnd", activeView: "activeView", cellUID: "cellUID", focusedDate: "focusedDate", isActive: "isActive", min: "min", max: "max", selectionRange: "selectionRange", selectedDates: "selectedDates", views: "views", showViewHeader: "showViewHeader", animateNavigation: "animateNavigation", orientation: "orientation", activeDescendant: "activeDescendant", tabIndex: "tabIndex", disabled: "disabled", id: "id", weekDaysFormat: "weekDaysFormat", weekNumber: "weekNumber" }, outputs: { cellClick: "cellClick", weekNumberCellClick: "weekNumberCellClick", cellEnter: "cellEnter", cellLeave: "cellLeave", activeDateChange: "activeDateChange", focusCalendar: "focusCalendar", blurCalendar: "blurCalendar", focusedCellChange: "focusedCellChange" }, host: { listeners: { "focus": "handleMultiViewCalendarFocus()", "blur": "handleMultiViewCalendarBlur($event)" }, properties: { "class.k-calendar-view": "this.getComponentClass", "class.k-align-items-start": "this.getComponentClass", "class.k-justify-content-center": "this.getComponentClass", "class.k-hstack": "this.horizontalHostClass", "class.k-vstack": "this.verticalHostClass", "class.k-calendar-monthview": "this.getComponentMonthClass", "class.k-calendar-yearview": "this.getComponentYearClass", "class.k-calendar-decadeview": "this.getComponentDecadeClass", "class.k-calendar-centuryview": "this.getComponentCenturyClass", "attr.role": "this.role", "attr.tabindex": "this.tabindex" } }, usesOnChanges: true, ngImport: i0, template: `
<ng-template #tableTemplate let-date="date" let-class="className">
<table
[attr.role]="views >= 2 ? 'none' : 'grid'"
class="k-calendar-table"
[ngClass]="class"
[attr.aria-labelledby]="id"
[attr.aria-activedescendant]="getActiveDescendant"
[attr.tabindex]="getTabIndex"
(focus)="handleClassicCalendarFocus()"
(blur)="handleClassicCalendarBlur($event)"
>
<caption *ngIf="showViewHeader" class="k-calendar-caption">{{ getCaptionTitle(date) }}</caption>
<thead *ngIf="isMonthView()" class="k-calendar-thead" role="rowgroup">
<tr class="k-calendar-tr" role="row">
<th *ngFor="let name of weekNames; let i = index;"
class="k-calendar-th"
scope="col"
[attr.aria-label]="wideWeekNames[i]"
role="columnheader"
>{{name}}</th>
</tr>
</thead>
<tbody
class="k-calendar-tbody"
kendoCalendarView
[allowReverse]="allowReverse"
[showOtherMonthDays]="showOtherMonthDays"
role="rowgroup"
direction="horizontal"
[activeView]="activeView"
[isActive]="isActive"
[min]="min"
[max]="max"
[cellUID]="cellUID"
[focusedDate]="focusedDate"
[selectedDates]="selectedDates"
[selectionRange]="selectionRange"
[activeRangeEnd]="activeRangeEnd"
[weekNumber]="weekNumber"
[templateRef]="cellTemplateRef"
[weekNumberTemplateRef]="weekNumberTemplateRef"
[viewDate]="date"
(cellClick)="cellClick.emit($event)"
(weekNumberCellClick)="weekNumberCellClick.emit($event)"
(cellEnter)="cellEnter.emit($event)"
(cellLeave)="cellLeave.emit($event)"
(focusedCellId)="focusedCellChange.emit($event)"
>
</tbody>
</table>
</ng-template>
<!-- When Next is clicked a placeholder table is rendered before the Main Table -->
<ng-template
*ngIf="nextAnimationDate"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: nextAnimationDate,
className: 'k-pointer-events-none'
}"
>
</ng-template>
<ng-template
*kFor="let date of dates"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: date
}"
>
</ng-template>
<!-- When Prev is clicked a placeholder table is rendered after the Main Table -->
<ng-template
*ngIf="prevAnimationDate"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: prevAnimationDate,
className: 'k-pointer-events-none'
}"
>
</ng-template>
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: ViewComponent, selector: "[kendoCalendarView]", inputs: ["allowReverse", "showOtherMonthDays", "direction", "isActive", "activeView", "cellUID", "focusedDate", "viewDate", "activeRangeEnd", "selectionRange", "min", "max", "selectedDates", "weekNumber", "viewIndex", "templateRef", "weekNumberTemplateRef", "headerTitle"], outputs: ["cellClick", "weekNumberCellClick", "cellEnter", "cellLeave", "focusedCellId"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: KForOf, selector: "[kFor][kForOf]", inputs: ["kForOf", "kForTrackBy", "kForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HorizontalViewListComponent, decorators: [{
type: Component,
args: [{
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'kendo-calendar-horizontal',
template: `
<ng-template #tableTemplate let-date="date" let-class="className">
<table
[attr.role]="views >= 2 ? 'none' : 'grid'"
class="k-calendar-table"
[ngClass]="class"
[attr.aria-labelledby]="id"
[attr.aria-activedescendant]="getActiveDescendant"
[attr.tabindex]="getTabIndex"
(focus)="handleClassicCalendarFocus()"
(blur)="handleClassicCalendarBlur($event)"
>
<caption *ngIf="showViewHeader" class="k-calendar-caption">{{ getCaptionTitle(date) }}</caption>
<thead *ngIf="isMonthView()" class="k-calendar-thead" role="rowgroup">
<tr class="k-calendar-tr" role="row">
<th *ngFor="let name of weekNames; let i = index;"
class="k-calendar-th"
scope="col"
[attr.aria-label]="wideWeekNames[i]"
role="columnheader"
>{{name}}</th>
</tr>
</thead>
<tbody
class="k-calendar-tbody"
kendoCalendarView
[allowReverse]="allowReverse"
[showOtherMonthDays]="showOtherMonthDays"
role="rowgroup"
direction="horizontal"
[activeView]="activeView"
[isActive]="isActive"
[min]="min"
[max]="max"
[cellUID]="cellUID"
[focusedDate]="focusedDate"
[selectedDates]="selectedDates"
[selectionRange]="selectionRange"
[activeRangeEnd]="activeRangeEnd"
[weekNumber]="weekNumber"
[templateRef]="cellTemplateRef"
[weekNumberTemplateRef]="weekNumberTemplateRef"
[viewDate]="date"
(cellClick)="cellClick.emit($event)"
(weekNumberCellClick)="weekNumberCellClick.emit($event)"
(cellEnter)="cellEnter.emit($event)"
(cellLeave)="cellLeave.emit($event)"
(focusedCellId)="focusedCellChange.emit($event)"
>
</tbody>
</table>
</ng-template>
<!-- When Next is clicked a placeholder table is rendered before the Main Table -->
<ng-template
*ngIf="nextAnimationDate"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: nextAnimationDate,
className: 'k-pointer-events-none'
}"
>
</ng-template>
<ng-template
*kFor="let date of dates"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: date
}"
>
</ng-template>
<!-- When Prev is clicked a placeholder table is rendered after the Main Table -->
<ng-template
*ngIf="prevAnimationDate"
[ngTemplateOutlet]="tableTemplate"
[ngTemplateOutletContext]="{
date: prevAnimationDate,
className: 'k-pointer-events-none'
}"
>
</ng-template>
`,
standalone: true,
imports: [NgClass, NgIf, NgFor, ViewComponent, NgTemplateOutlet, KForOf]
}]
}], ctorParameters: function () { return [{ type: i1.BusViewService }, { type: i2.IntlService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }]; }, propDecorators: { handleMultiViewCalendarFocus: [{
type: HostListener,
args: ["focus"]
}], handleMultiViewCalendarBlur: [{
type: HostListener,
args: ["blur", ['$event']]
}], showOtherMonthDays: [{
type: Input
}], cellTemplateRef: [{
type: Input
}], weekNumberTemplateRef: [{
type: Input
}], allowReverse: [{
type: Input
}], activeRangeEnd: [{
type: Input
}], activeView: [{
type: Input
}], cellUID: [{
type: Input
}], focusedDate: [{
type: Input
}], isActive: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], selectionRange: [{
type: Input
}], selectedDates: [{
type: Input
}], views: [{
type: Input
}], showViewHeader: [{
type: Input
}], animateNavigation: [{
type: Input
}], orientation: [{
type: Input
}], activeDescendant: [{
type: Input
}], tabIndex: [{
type: Input
}], disabled: [{
type: Input
}], id: [{
type: Input
}], weekDaysFormat: [{
type: Input
}], weekNumber: [{
type: Input
}], cellClick: [{
type: Output
}], weekNumberCellClick: [{
type: Output
}], cellEnter: [{
type: Output
}], cellLeave: [{
type: Output
}], activeDateChange: [{
type: Output
}], focusCalendar: [{
type: Output
}], blurCalendar: [{
type: Output
}], focusedCellChange: [{
type: Output
}], getComponentClass: [{
type: HostBinding,
args: ["class.k-calendar-view"]
}, {
type: HostBinding,
args: ["class.k-align-items-start"]
}, {
type: HostBinding,
args: ["class.k-justify-content-center"]
}], horizontalHostClass: [{
type: HostBinding,
args: ["class.k-hstack"]
}], verticalHostClass: [{
type: HostBinding,
args: ["class.k-vstack"]
}], getComponentMonthClass: [{
type: HostBinding,
args: ["class.k-calendar-monthview"]
}], getComponentYearClass: [{
type: HostBinding,
args: ["class.k-calendar-yearview"]
}], getComponentDecadeClass: [{
type: HostBinding,
args: ["class.k-calendar-decadeview"]
}], getComponentCenturyClass: [{
type: HostBinding,
args: ["class.k-calendar-centuryview"]
}], role: [{
type: HostBinding,
args: ['attr.role']
}], tabindex: [{
type: HostBinding,
args: ['attr.tabindex']
}] } });