UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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'] }] } });