@progress/kendo-angular-scheduler
Version:
Kendo UI Scheduler Angular - Outlook or Google-style angular scheduler calendar. Full-featured and customizable embedded scheduling from the creator developers trust for professional UI components.
425 lines (424 loc) • 19.7 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Input, TemplateRef, ViewChildren, QueryList, ElementRef, ChangeDetectorRef, NgZone, Renderer2, Component } from '@angular/core';
import { isChanged, isDocumentAvailable, ScrollbarWidthService } from '@progress/kendo-angular-common';
import { addDays, getDate, ZonedDate } from '@progress/kendo-date-math';
import { fromEvent } from 'rxjs';
import { toInvariantTime, dateInRange, setCoordinates, dateWithTime, toUTCTime, toUTCDateTime } from '../utils';
import { createTimeSlots } from './utils';
import { DayTimeSlotService } from './day-time-slot.service';
import { ViewContextService } from '../view-context.service';
import { ViewStateService } from '../view-state.service';
import { IntlService } from '@progress/kendo-angular-intl';
import { PDFService } from '../../pdf/pdf.service';
import { BaseView } from '../common/base-view';
import { MIDNIGHT_INVARIANT, MS_PER_MINUTE, ONGOING_EVENT_CSS_CLASS } from '../constants';
import { fromClick, fromDoubleClick, isNumber, isPresent } from '../../common/util';
import { rtlScrollPosition, hasClasses } from '../../common/dom-queries';
import { LocalizationService } from '@progress/kendo-angular-l10n';
import { DayTimeViewItemComponent } from './day-time-view-item.component';
import * as i0 from "@angular/core";
import * as i1 from "../view-context.service";
import * as i2 from "../view-state.service";
import * as i3 from "@progress/kendo-angular-intl";
import * as i4 from "./day-time-slot.service";
import * as i5 from "../../pdf/pdf.service";
import * as i6 from "@progress/kendo-angular-l10n";
import * as i7 from "@progress/kendo-angular-common";
const getStartDate = date => getDate(date);
const getEndDate = (start, numberOfDays) => getDate(addDays(start, numberOfDays || 1));
const getNextDate = (date, count, numberOfDays) => getDate(addDays(date, numberOfDays * count));
/**
* @hidden
*/
export class DayTimeViewComponent extends BaseView {
changeDetector;
timeSlotTemplate;
dateHeaderTemplate;
majorTimeHeaderTemplate;
minorTimeHeaderTemplate;
numberOfDays = 1;
scrollTime;
startTime = '00:00';
endTime = '00:00';
workDayStart = '08:00';
workDayEnd = '17:00';
workWeekStart = 1;
workWeekEnd = 5;
slotDuration = 60;
slotDivisions = 2;
showWorkHours = false;
getStartDate = getStartDate;
getEndDate = getEndDate;
getNextDate = getNextDate;
currentTimeMarker;
highlightOngoingEvents;
currentTimeElements;
eventElements;
currentTimeArrows;
name;
workDayStartTime;
workDayEndTime;
daySlots = [];
timeSlots = [];
resizeHintFormat = 't';
showCurrentTime = false;
get classNames() {
return `k-scheduler-${this.name}view`;
}
get timeSlotTemplateRef() {
return this.timeSlotTemplate || (this.schedulerTimeSlotTemplate || {}).templateRef;
}
get dateHeaderTemplateRef() {
return this.dateHeaderTemplate || (this.schedulerDateHeaderTemplate || {}).templateRef;
}
get majorTimeHeaderTemplateRef() {
return this.majorTimeHeaderTemplate || (this.schedulerMajorTimeHeaderTemplate || {}).templateRef;
}
get minorTimeHeaderTemplateRef() {
return this.minorTimeHeaderTemplate || (this.schedulerMinorTimeHeaderTemplate || {}).templateRef;
}
schedulerTimeSlotTemplate;
schedulerDateHeaderTemplate;
schedulerMajorTimeHeaderTemplate;
schedulerMinorTimeHeaderTemplate;
currentTimeTimeout;
ongoingEventsTimeout;
currentDate;
verticalTime = true;
initialUpdate = true;
constructor(changeDetector, viewContext, viewState, intl, slotService, zone, renderer, element, pdfService, localization, scrollBarWidthService) {
super(viewContext, viewState, intl, slotService, zone, renderer, element, pdfService, localization, changeDetector, scrollBarWidthService);
this.changeDetector = changeDetector;
this.updateCurrentTime = this.updateCurrentTime.bind(this);
this.toggleOngoingClass = this.toggleOngoingClass.bind(this);
this.updateOngoingEvents = this.updateOngoingEvents.bind(this);
}
ngOnChanges(changes) {
if (changes.startTime || changes.endTime || changes.showWorkHours || changes.workDayStart || changes.workDayEnd ||
changes.workWeekStart || changes.workWeekEnd || changes.slotDivisions || changes.slotDuration) {
this.timeSlots = this.createTimeSlots();
this.initWorkDay();
this.changes.next(null);
}
if (isChanged('currentTimeMarker', changes)) {
this.showCurrentTime = this.enableCurrentTime();
}
if (isChanged('weekStart', changes)) {
this.onSelectDate(this.selectedDate);
}
super.ngOnChanges(changes);
}
ngOnDestroy() {
super.ngOnDestroy();
clearTimeout(this.currentTimeTimeout);
clearTimeout(this.ongoingEventsTimeout);
}
verticalItem(leafIndex, resourceIndex) {
const data = this.verticalResources[resourceIndex].data || [];
const resources = this.verticalResources;
let result = 1;
for (let idx = resourceIndex + 1; idx < resources.length; idx++) {
result *= ((resources[idx].data || []).length || 1);
}
return data[(leafIndex / result) % data.length];
}
timeSlotClass(slot, date, resourceIndex) {
if (this.slotClass) {
return this.slotClass({
start: dateWithTime(date, slot.start),
end: dateWithTime(date, slot.end),
resources: this.resourcesByIndex(resourceIndex),
isAllDay: false
});
}
}
toggleOngoingClass() {
const now = this.currentTime();
const cssClass = isPresent(this.highlightOngoingEvents.cssClass) ? this.highlightOngoingEvents.cssClass : ONGOING_EVENT_CSS_CLASS;
this.eventElements.forEach((event) => {
const isOngoing = dateInRange(now, event.item.start, event.item.end);
this.renderer[isOngoing ? 'addClass' : 'removeClass'](event.nativeElement, cssClass);
});
}
scrollToTime(time = this.scrollTime) {
let date;
if (typeof time === 'string') {
const scrollDate = this.intl.parseDate(time);
if (!scrollDate) {
return;
}
date = toUTCTime(this.daySlots[0].start, scrollDate);
}
else {
date = toUTCDateTime(time);
}
const position = this.slotService.timePosition(date, 0, this.verticalTime);
if (isNumber(position)) {
const contentElement = this.content.nativeElement;
contentElement[this.verticalTime ? 'scrollTop' : 'scrollLeft'] =
(this.localization.rtl && !this.verticalTime) ? rtlScrollPosition(contentElement, position) : position;
}
}
optionsChange(options) {
this.schedulerTimeSlotTemplate = options.timeSlotTemplate;
this.schedulerDateHeaderTemplate = options.dateHeaderTemplate;
this.schedulerMajorTimeHeaderTemplate = options.majorTimeHeaderTemplate;
this.schedulerMinorTimeHeaderTemplate = options.minorTimeHeaderTemplate;
super.optionsChange(options);
}
updateView() {
super.updateView();
this.updateCurrentTime();
this.updateOngoingEvents();
if (this.initialUpdate) {
this.scrollToTime();
this.initialUpdate = false;
}
}
enableCurrentTime() {
if (!this.currentTimeMarker || this.currentTimeMarker.enabled === false || !this.selectedDate) {
return false;
}
const dateRange = this.dateRange();
this.currentDate = ZonedDate.fromLocalDate(this.currentTime(), this.currentTimeMarker.localTimezone !== false ? '' : this.timezone);
const localTime = this.currentDate.toLocalDate();
const invariantTime = toInvariantTime(localTime);
const timeSlots = this.timeSlots;
const inDateRange = dateInRange(localTime, dateRange.start, dateRange.end);
const inTimeRange = timeSlots.length && dateInRange(invariantTime, timeSlots[0].start, timeSlots[timeSlots.length - 1].end);
return inDateRange && inTimeRange;
}
currentTime() {
return new Date();
}
updateCurrentTime() {
if (!isDocumentAvailable()) {
return;
}
const enable = this.enableCurrentTime();
if (enable !== this.showCurrentTime) {
this.showCurrentTime = enable;
this.changeDetector.detectChanges();
}
clearTimeout(this.currentTimeTimeout);
if (enable) {
this.zone.runOutsideAngular(() => {
this.currentTimeTimeout = setTimeout(this.updateCurrentTime, this.currentTimeMarker.updateInterval || MS_PER_MINUTE);
});
this.positionCurrentTime();
}
}
updateOngoingEvents() {
const disabled = !this.highlightOngoingEvents || this.highlightOngoingEvents.enabled === false;
if (!isDocumentAvailable() || disabled) {
return;
}
clearTimeout(this.ongoingEventsTimeout);
this.zone.runOutsideAngular(() => {
this.ongoingEventsTimeout = setTimeout(this.updateOngoingEvents, this.highlightOngoingEvents.updateInterval || MS_PER_MINUTE);
});
this.toggleOngoingClass();
}
positionCurrentTime() {
if (this.currentTimeElements && this.currentTimeElements.length) {
const date = this.currentDate.toUTCDate();
const currentTimeArrows = this.currentTimeArrows ? this.currentTimeArrows.toArray() : [];
const arrowOffset = currentTimeArrows.length ? this.currentTimeArrowOffset() : 0;
const arrowMid = currentTimeArrows.length ? (currentTimeArrows[0].nativeElement.offsetHeight / 2) : 4;
const tableWidth = this.contentTable.nativeElement.clientWidth;
const tableHeight = this.contentTable.nativeElement.clientHeight;
const vertical = this.verticalTime;
this.currentTimeElements.forEach((element, index) => {
const position = this.slotService.timePosition(date, index, vertical);
if (position !== undefined) {
const line = element.nativeElement;
if (currentTimeArrows[index]) {
const arrow = currentTimeArrows[index].nativeElement;
const origin = vertical ? arrowOffset : position - arrowMid;
setCoordinates(arrow, {
top: vertical ? position - arrowMid : arrowOffset,
left: origin,
right: origin
});
}
const origin = vertical ? 0 : position;
setCoordinates(line, {
top: vertical ? position : 0,
left: origin,
right: origin,
width: vertical ? tableWidth : 1,
height: vertical ? 1 : tableHeight
});
}
});
}
}
bindEvents() {
super.bindEvents();
this.zone.runOutsideAngular(() => {
this.subs.add(fromClick(this.headerWrap.nativeElement)
.subscribe(e => this.onHeaderClick(e)));
this.subs.add(fromEvent(this.headerWrap.nativeElement, 'contextmenu')
.subscribe(e => this.onClick(e)));
this.subs.add(fromDoubleClick(this.headerWrap.nativeElement)
.subscribe(e => this.onClick(e, 'dblclick')));
});
}
onHeaderClick(e) {
this.onClick(e);
if (this.daySlots.length <= 1) {
return;
}
const daySlotIndex = e.target.getAttribute('data-dayslot-index');
if (daySlotIndex) {
const slot = this.daySlots[parseInt(daySlotIndex, 10)];
this.zone.run(() => {
this.viewState.navigateTo({ viewName: 'day', date: slot.start });
});
}
}
slotByIndex(slotIndex, args) {
return this.slotService.slotByIndex(slotIndex, args.target.hasAttribute('data-day-slot'));
}
onSelectDate(date) {
this.selectedDate = date;
this.daySlots = this.createDaySlots();
this.showCurrentTime = this.enableCurrentTime();
this.viewState.notifyDateRange(this.dateRange());
}
onAction(e) {
const now = getDate(this.selectedDate);
if (e.type === 'next') {
const next = this.getNextDate(now, 1, this.numberOfDays);
if (this.isInRange(next)) {
this.viewState.notifyNextDate(next);
}
}
if (e.type === 'prev') {
const next = this.getNextDate(now, -1, this.numberOfDays);
if (this.isInRange(next)) {
this.viewState.notifyNextDate(next);
}
}
if (e.type === 'scroll-time') {
this.scrollToTime(e.time);
}
}
dateRange(date = this.selectedDate) {
const start = this.getStartDate(date);
const end = this.getEndDate(start, this.numberOfDays);
const rangeEnd = this.getEndDate(start, this.numberOfDays - 1);
const text = this.intl.format(this.selectedDateFormat, start, rangeEnd);
const shortText = this.intl.format(this.selectedShortDateFormat, start, rangeEnd);
return { start, end, text, shortText };
}
createDaySlots() {
let current = this.getStartDate(this.selectedDate);
const end = this.getEndDate(current, this.numberOfDays);
const dates = [];
while (current < end) {
const next = addDays(current, 1);
dates.push({
start: current,
end: next
});
current = next;
}
return dates;
}
createTimeSlots() {
return createTimeSlots(this.intl, {
showWorkHours: this.showWorkHours,
startTime: this.startTime,
endTime: this.endTime,
workDayStart: this.workDayStart,
workDayEnd: this.workDayEnd,
slotDivisions: this.slotDivisions,
slotDuration: this.slotDuration
});
}
initWorkDay() {
const startDate = this.intl.parseDate(this.workDayStart);
this.workDayStartTime = toInvariantTime(startDate);
const endDate = this.intl.parseDate(this.workDayEnd);
if (endDate <= startDate) {
this.workDayEndTime = addDays(MIDNIGHT_INVARIANT, 1);
}
else {
this.workDayEndTime = toInvariantTime(endDate);
}
}
slotByPosition(x, y, container) {
const isDaySlot = container ? hasClasses(container.parentNode, 'k-scheduler-header-wrap') : y < 0;
return this.slotService.slotByPosition(x, y, isDaySlot, Boolean(this.verticalResources.length));
}
slotFields(slot) {
const fields = super.slotFields(slot);
if (slot.isDaySlot) {
fields.isAllDay = true;
}
else {
fields.start = this.convertDate(slot.start);
fields.end = this.convertDate(slot.end);
}
return fields;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DayTimeViewComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.ViewContextService }, { token: i2.ViewStateService }, { token: i3.IntlService }, { token: i4.DayTimeSlotService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i5.PDFService }, { token: i6.LocalizationService }, { token: i7.ScrollbarWidthService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: DayTimeViewComponent, selector: "kendo-day-time-view", inputs: { timeSlotTemplate: "timeSlotTemplate", dateHeaderTemplate: "dateHeaderTemplate", majorTimeHeaderTemplate: "majorTimeHeaderTemplate", minorTimeHeaderTemplate: "minorTimeHeaderTemplate", numberOfDays: "numberOfDays", scrollTime: "scrollTime", startTime: "startTime", endTime: "endTime", workDayStart: "workDayStart", workDayEnd: "workDayEnd", workWeekStart: "workWeekStart", workWeekEnd: "workWeekEnd", slotDuration: "slotDuration", slotDivisions: "slotDivisions", showWorkHours: "showWorkHours", getStartDate: "getStartDate", getEndDate: "getEndDate", getNextDate: "getNextDate", currentTimeMarker: "currentTimeMarker", highlightOngoingEvents: "highlightOngoingEvents" }, viewQueries: [{ propertyName: "currentTimeElements", predicate: ["currentTimeMarker"], descendants: true }, { propertyName: "eventElements", predicate: DayTimeViewItemComponent, descendants: true }, { propertyName: "currentTimeArrows", predicate: ["currentTimeArrow"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DayTimeViewComponent, decorators: [{
type: Component,
args: [{ selector: 'kendo-day-time-view', template: '' }]
}], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i1.ViewContextService }, { type: i2.ViewStateService }, { type: i3.IntlService }, { type: i4.DayTimeSlotService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i5.PDFService }, { type: i6.LocalizationService }, { type: i7.ScrollbarWidthService }]; }, propDecorators: { timeSlotTemplate: [{
type: Input
}], dateHeaderTemplate: [{
type: Input
}], majorTimeHeaderTemplate: [{
type: Input
}], minorTimeHeaderTemplate: [{
type: Input
}], numberOfDays: [{
type: Input
}], scrollTime: [{
type: Input
}], startTime: [{
type: Input
}], endTime: [{
type: Input
}], workDayStart: [{
type: Input
}], workDayEnd: [{
type: Input
}], workWeekStart: [{
type: Input
}], workWeekEnd: [{
type: Input
}], slotDuration: [{
type: Input
}], slotDivisions: [{
type: Input
}], showWorkHours: [{
type: Input
}], getStartDate: [{
type: Input
}], getEndDate: [{
type: Input
}], getNextDate: [{
type: Input
}], currentTimeMarker: [{
type: Input
}], highlightOngoingEvents: [{
type: Input
}], currentTimeElements: [{
type: ViewChildren,
args: ['currentTimeMarker']
}], eventElements: [{
type: ViewChildren,
args: [DayTimeViewItemComponent]
}], currentTimeArrows: [{
type: ViewChildren,
args: ['currentTimeArrow']
}] } });