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.

1,097 lines (1,096 loc) 46 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ViewChild, NgZone, ElementRef, Renderer2, Input, TemplateRef, ChangeDetectorRef, Directive } from '@angular/core'; import { anyChanged, scrollbarWidth, guid, ScrollbarWidthService } from '@progress/kendo-angular-common'; import { IntlService } from '@progress/kendo-angular-intl'; import { Day, ZonedDate } from '@progress/kendo-date-math'; import { Subscription, BehaviorSubject, combineLatest, fromEvent, merge } from 'rxjs'; import { switchMap, take, map, filter, tap } from 'rxjs/operators'; import { closestInScope, hasClasses, preventLockedScroll, wheelDeltaY, hasScrollbar } from '../../common/dom-queries'; import { groupResources, getField, setField, fromClick, fromDoubleClick } from '../../common/util'; import { BaseSlotService } from '../view-items/base-slot.service'; import { ViewContextService } from '../view-context.service'; import { ViewStateService } from '../view-state.service'; import { assignTasksResources, toPx, elementOffset, pointDistance, ignoreContentChild, resourceItemByValue, convertNgClassBindings, toUTCDateTime, dateWithTime, normaliseRangeStartAndEnd } from '../utils'; import { Draggable } from '@progress/kendo-draggable'; import { HintContainerComponent } from '../common/hint-container.component'; import { PDFService } from '../../pdf/pdf.service'; import { LocalizationService } from '@progress/kendo-angular-l10n'; import { slotDragEventName } from '../../types/slot-selection'; import { SlotDragStartEvent } from '../../events/slot-drag-start-event'; 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 "../view-items/base-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 SCROLL_CHANGE = 15; const MIN_DISTANCE = 60; const SCROLL_INTERVAL = 50; const MIN_MOVE_DISTANCE = 10; /** @hidden */ export class BaseView { viewContext; viewState; intl; slotService; zone; renderer; element; pdfService; localization; cdr; scrollBarWidthService; eventTemplate; groupHeaderTemplate; selectedDateFormat; selectedShortDateFormat; eventHeight; showToolbar; showFooter; slotClass; eventClass; eventStyles; weekStart; content; header; contentTable; times; timesHeader; timesTable; headerWrap; hintContainer; get eventTemplateRef() { return this.eventTemplate || (this.schedulerEventTemplate || {}).templateRef; } get groupHeaderTemplateRef() { return this.groupHeaderTemplate || (this.schedulerGroupHeaderTemplate || {}).templateRef; } /** * The non-all-day events. */ items = new BehaviorSubject(null); horizontalResources = []; verticalResources = []; dragHints = []; resizeHints = []; editable; selectable; // initialized to false in the scheduler component getField = getField; changes = new BehaviorSubject(null); viewRangeChange = new BehaviorSubject(null); subs = new Subscription(); groupedResources = []; spans = []; contentHeight; /** * All events that the user provided. */ tasks; group; resources; domEvents = []; schedulerEventTemplate; schedulerGroupHeaderTemplate; min; max; selectedDate; resourcesCache = {}; timezone; draggable; /** * The event which is currently being resized. */ resizing; dragging; dragArgs; /** * The slot which is currently being dragged over while selecting a range of slots. */ dragSelecting = null; /** * The slot where the drag-selecting originated. Used for flipping the start and end of the emitted range. */ dragSelectOrigin = null; container; containerOffset; pressLocation; pressTarget; scrollInterval; autoHeight = false; rtl = false; isSlotSelected = () => false; constructor(viewContext, viewState, intl, slotService, zone, renderer, element, pdfService, localization, cdr, scrollBarWidthService) { this.viewContext = viewContext; this.viewState = viewState; this.intl = intl; this.slotService = slotService; this.zone = zone; this.renderer = renderer; this.element = element; this.pdfService = pdfService; this.localization = localization; this.cdr = cdr; this.scrollBarWidthService = scrollBarWidthService; this.setSlotClass = this.setSlotClass.bind(this); this.setHintClass = this.setHintClass.bind(this); this.weekStart = intl.firstDay(); } /** * Generates a list of space-separated IDs based on a collection of items to associate scrollable containers * with the respective tasks through the aria-owns attribute for a11y compliance. */ matchOwned(items) { const ids = items.reduce((acc, curr) => [...acc, ...curr.resources.map(ir => `${curr.elementId}_${ir.leafIdx}`)], []); return ids.join(' '); } ngOnInit() { const updateView = this.updateView.bind(this); this.resourcesByIndex = this.resourcesByIndex.bind(this); this.subs.add(this.viewContext.selectedDate.subscribe(this.onSelectDate.bind(this))); this.subs.add(this.viewState.optionsChange.subscribe(this.onStateOptionsChange.bind(this))); this.subs.add(this.viewContext.action.subscribe(this.onAction.bind(this))); this.subs.add(this.viewContext.execute.subscribe(this.execute.bind(this))); this.subs.add(this.viewContext.resize.subscribe(() => { this.toggleElement(false); this.updateView(); })); this.subs.add(this.viewContext.optionsChange.subscribe(this.optionsChange.bind(this))); this.subs.add(this.changes.subscribe(() => { this.toggleElement(false); })); this.subs.add(combineLatest(this.viewContext.items, this.viewState.dateRange, this.viewRangeChange) .pipe(map(([items, dateRange]) => this.createTasks(items, dateRange))) .subscribe((tasks) => { this.tasks = tasks.map(t => ({ ...t, elementId: guid() })); this.assignResources(); this.onTasksChange(); })); this.subs.add(combineLatest(this.items, this.changes, this.localization.changes).pipe(switchMap(() => this.onStable())) .subscribe(updateView)); this.subs.add(this.pdfService.createElement.subscribe(this.createPDFElement.bind(this))); } ngOnChanges(changes) { if (anyChanged(['selectedDateFormat', 'selectedShortDateFormat'], changes)) { this.viewState.notifyDateRange(this.dateRange(this.selectedDate)); } if (changes.eventHeight) { this.changes.next(null); } } ngAfterViewInit() { this.bindEvents(); this.subs.add(this.localization.changes.subscribe(({ rtl }) => { this.rtl = rtl; })); } ngOnDestroy() { this.subs.unsubscribe(); this.domEvents.forEach(unbindHandler => unbindHandler()); this.domEvents = []; if (this.draggable) { this.draggable.destroy(); this.draggable = null; } } itemIndex(index, _) { return index; } resourcesByIndex(index) { if (!this.resources) { return []; } if (!this.group) { // When we have resources, but are not grouped, return all resources. return this.resources.reduce((result, resource) => result.concat(resource.data || []), []); } if (!this.resourcesCache[index]) { const resources = this.taskResources; const result = []; let currentIndex = index; for (let idx = 0; idx < resources.length; idx++) { const data = resources[idx].data || []; const dataIdx = Math.floor(currentIndex / this.spans[idx]); result.push(data[dataIdx]); currentIndex -= dataIdx * this.spans[idx]; } this.resourcesCache[index] = result; } return this.resourcesCache[index]; } dragResourcesByIndex(index) { const allResources = this.resources || []; const result = []; if (this.groupedResources.length) { const resources = this.resourcesByIndex(index).slice(0); const taskResources = this.taskResources; for (let idx = 0; idx < taskResources.length; idx++) { const index = this.resources.indexOf(taskResources[idx]); if (index >= 0) { result[index] = resources[idx]; } } } for (let idx = 0; idx < allResources.length; idx++) { if (!result[idx]) { result[idx] = resourceItemByValue(this.dragging.task.event, allResources[idx]); } } return result; } getEventClasses(item, resources, isAllDay) { if (this.eventClass) { return this.eventClass({ event: item.event, resources, isAllDay }); } } getEventStyles(item, itemResource, isAllDay) { const result = { backgroundColor: itemResource.color, borderColor: itemResource.color }; if (this.eventStyles) { Object.assign(result, this.eventStyles({ event: item.event, resources: itemResource.resources, isAllDay })); } return result; } // Similar to https://tc39.es/proposal-temporal/docs/plaindate.html // Stores a "plain date" in the UTC parts of a regular `Date`. toPlainDate(date) { const result = toUTCDateTime(this.convertDate(date)); return result; } // Similar to https://tc39.es/proposal-temporal/docs/plaindatetime.html // Stores a "plain date-time" in the UTC parts of a regular `Date`. toPlainDateTime(date, time) { if (!date || !time) { return null; } return this.toPlainDate(dateWithTime(date, time)); } optionsChange(options) { this.schedulerEventTemplate = options.eventTemplate; this.schedulerGroupHeaderTemplate = options.groupHeaderTemplate; this.min = options.min; this.max = options.max; this.editable = options.editable; this.timezone = options.timezone; this.selectable = options.selectable; this.isSlotSelected = options.isSlotSelected; if (!options.changes || anyChanged(['group', 'resources'], options.changes, false) || options.group !== this.group || options.resources !== this.resources) { this.group = options.group; this.resources = options.resources; this.groupResources(); this.resourcesCache = {}; if (this.tasks && this.tasks.length) { this.assignResources(); this.onTasksChange(); } this.changes.next(null); } this.onStateOptionsChange(options); } toggleElement(visible) { if (this.element) { this.renderer.setStyle(this.element.nativeElement, 'display', visible ? 'block' : 'none'); } } onStable() { return this.zone.onStable.asObservable().pipe(take(1)); } updateView() { this.slotService.invalidate(); this.toggleElement(true); this.reflow(); this.viewState.notifyLayoutEnd(); this.viewState.emitEvent(slotDragEventName.refreshSlotSelection); } assignResources() { assignTasksResources(this.tasks, { taskResources: this.taskResources, hasGroups: this.groupedResources.length > 0, allResources: this.resources, spans: this.spans }); } bindEvents() { const contentElement = this.content.nativeElement; const element = this.element.nativeElement; this.zone.runOutsideAngular(() => { if (this.times) { this.subs.add(merge(fromEvent(this.times.nativeElement, 'mousewheel'), fromEvent(this.times.nativeElement, 'DOMMouseScroll')).pipe(filter((event) => !event.ctrlKey), tap(preventLockedScroll(contentElement)), map(wheelDeltaY)) .subscribe(x => contentElement.scrollTop -= x)); } this.subs.add(merge(fromClick(contentElement), fromEvent(contentElement, 'contextmenu')) .subscribe(e => this.onClick(e))); this.subs.add(fromDoubleClick(contentElement) .subscribe(e => this.onClick(e, 'dblclick'))); this.subs.add(fromEvent(element, 'keydown') .subscribe(e => this.onKeydown(e))); this.domEvents.push(this.renderer.listen(contentElement, 'scroll', () => { if (this.headerWrap) { this.headerWrap.nativeElement.scrollLeft = contentElement.scrollLeft; } if (this.times) { this.times.nativeElement.scrollTop = contentElement.scrollTop; } })); this.draggable = new Draggable({ press: this.onPress.bind(this), drag: this.onDrag.bind(this), release: this.onRelease.bind(this) }); this.draggable.bindTo(element); }); } onPress(args) { const editResizable = this.editable && this.editable.resize !== false; const editDraggable = this.editable && this.editable.drag !== false; const target = args.originalEvent.target; if (hasClasses(target, 'k-resize-handle')) { if (!editResizable) { return; } this.initResize(args); } else if (editDraggable) { const task = this.targetTask(target); if (task) { if (!args.isTouch) { args.originalEvent.preventDefault(); } this.pressLocation = { x: args.pageX, y: args.pageY }; this.pressTarget = task; } } const notDraggingEvent = !this.pressTarget; if (notDraggingEvent && this.selectable) { //fixes https://github.com/telerik/kendo-angular/issues/4446 args.originalEvent.preventDefault(); this.initDragSelect(args); } this.dragArgs = args; } onDrag(args) { if (this.resizing) { this.resize(args); this.scrollContainer(this.resize, args); } else { this.initDrag(args); if (this.dragging) { this.drag(args); args.originalEvent.preventDefault(); this.scrollContainer(this.drag, args); } if (this.dragSelecting) { this.dragSelect(args); } } this.dragArgs = args; } onRelease(args) { clearInterval(this.scrollInterval); const { resizing, dragging, dragSelecting } = this; if (resizing) { this.emitEvent('resizeEnd', { event: resizing.task.event, dataItem: resizing.task.event.dataItem, start: this.convertDate(resizing.start), end: this.convertDate(resizing.end) }); this.resizeHints = []; } if (dragging) { this.emitEvent('dragEnd', { event: dragging.task.event, dataItem: dragging.task.event.dataItem, start: dragging.start ? this.convertDate(dragging.start) : dragging.task.start.toLocalDate(), end: dragging.end ? this.convertDate(dragging.end) : dragging.task.end.toLocalDate(), resources: dragging.resources, isAllDay: this.draggedIsAllDay(dragging.task, dragging.slot) }); this.dragHints = []; } if (resizing || dragging) { this.removeSlotClass(); this.updateHintContainer(); this.resizing = null; this.dragging = null; } if (dragSelecting) { const { start, end } = normaliseRangeStartAndEnd(this.dragSelectOrigin, this.dragSelecting); const range = { start: this.convertDate(start), end: this.convertDate(end), isAllDay: this.dragSelecting.isDaySlot, resources: this.resourcesByIndex(this.dragSelecting.id.resourceIndex), originalEvent: args.originalEvent }; this.viewState.notifySlotSelectionEnd(range); this.dragSelectOrigin = null; this.dragSelecting = null; this.cdr.markForCheck(); } this.container = null; this.dragArgs = null; this.pressLocation = null; this.pressTarget = null; } setHintClass(className) { (this.dragging || this.resizing).hintClass = className; } updateHintClass() { const current = this.dragging || this.resizing; let update = false; this.hints.forEach(hint => { if (hint.class !== current.hintClass) { hint.class = current.hintClass; update = true; } }); if (update) { this.updateHintContainer(); } } removeHintClass() { (this.dragging || this.resizing).hintClass = null; } setSlotClass(className) { const current = this.dragging || this.resizing; current.slotClass = className; this.renderer.addClass(current.slot.nativeElement, current.slotClass); } removeSlotClass() { const current = this.dragging || this.resizing; if (current.slotClass) { this.renderer.removeClass(current.slot.nativeElement, current.slotClass); current.slotClass = null; } } get hints() { return this.dragging ? this.dragHints : this.resizeHints; } initDrag(args) { if (!this.dragging && this.pressLocation && pointDistance(this.pressLocation.x, this.pressLocation.y, args.pageX, args.pageY) >= MIN_MOVE_DISTANCE) { const dragging = this.pressTarget; const task = dragging.task; if (this.emitEvent('dragStart', { event: task.event, dataItem: task.event.dataItem })) { this.pressLocation = null; this.pressTarget = null; return; } this.updateDragContainer(args); if (this.containerOffset === undefined) { return; } const { x, y } = this.coordinatesOffset(this.pressLocation.x, this.pressLocation.y); const slot = this.slotByPosition(Math.ceil(x), Math.ceil(y), this.container); if (!slot) { return; } this.dragging = dragging; this.dragging.offset = { start: slot.start.getTime() - task.start.toUTCDate().getTime(), end: task.end.toUTCDate().getTime() - slot.start.getTime() }; this.dragging.slot = slot; this.dragging.startResources = this.dragging.resourceItems = this.dragResourcesByIndex(slot.id.resourceIndex); this.dragging.resources = this.resourceValues(task, this.dragging.startResources); } } updateDragContainer(_args) { if (!this.container) { this.container = this.content.nativeElement; this.containerOffset = elementOffset(this.container); } } drag(args) { this.updateDragContainer(args); if (!this.container) { return; } const { x, y } = this.coordinatesOffset(args.pageX, args.pageY); const slot = this.slotByPosition(x, y, this.container); if (slot && (slot !== this.dragging.slot || !this.dragHints.length)) { const dragging = this.dragging; const { slot: currentSlot, task } = dragging; const { ranges, start, end, isAllDay } = this.dragRanges(slot); let resourceItems, resourceValues; if (currentSlot.id.resourceIndex !== slot.id.resourceIndex) { resourceItems = this.dragResourcesByIndex(slot.id.resourceIndex); resourceValues = this.resourceValues(task, resourceItems); } else { resourceItems = dragging.resourceItems; resourceValues = dragging.resources; } this.removeSlotClass(); dragging.start = start; dragging.end = end; dragging.slot = slot; dragging.resources = resourceValues; dragging.resourceItems = resourceItems; dragging.hintClass = null; if (this.emitEvent('drag', { event: task.event, dataItem: task.event.dataItem, start: this.convertDate(start), end: this.convertDate(end), resources: resourceValues, isAllDay, setHintClass: this.setHintClass, setSlotClass: this.setSlotClass })) { this.updateHintClass(); return; } const color = this.dragResourceColor(task, resourceItems); const hintClasses = this.dragHintClasses(); this.dragHints = []; for (let idx = 0; idx < ranges.length; idx++) { const slots = ranges[idx]; const first = slots[0]; const last = slots[slots.length - 1]; const size = this.dragHintSize(first, last); const origin = first.rect.left; const styles = { top: toPx(first.rect.top), left: this.localization.rtl ? '' : toPx(origin), right: !this.localization.rtl ? '' : toPx(origin), width: size.width, height: size.height, backgroundColor: color, borderColor: color }; if (this.eventStyles) { Object.assign(styles, this.eventStyles(this.dragHintEventStyleArgs())); } this.dragHints.push({ item: Object.assign({}, this.dragging.task, { startTime: start, endTime: end }), resources: resourceItems, class: hintClasses, style: styles }); } this.updateHintContainer(); } } initDragSelect(args) { this.updateDragContainer(args); if (!this.containerOffset) { return; } const { x, y } = this.coordinatesOffset(args.pageX, args.pageY); const slot = this.slotByPosition(x, y, this.container); if (!slot) { return; } const range = { start: this.convertDate(slot.start), end: this.convertDate(slot.end), isAllDay: slot.isDaySlot, resources: this.resourcesByIndex(slot.id.resourceIndex), originalEvent: args.originalEvent }; const slotDragStartEvent = new SlotDragStartEvent(null, range); this.viewState.notifySlotSelectionStart(slotDragStartEvent); if (!slotDragStartEvent.isDefaultPrevented()) { this.dragSelecting = slot; this.dragSelectOrigin = slot; this.cdr.markForCheck(); } } dragSelect(args) { const { x, y } = this.coordinatesOffset(args.pageX, args.pageY); const slot = this.slotByPosition(x, y, this.container); if (!slot) { return; } if (this.dragSelecting !== slot) { this.dragSelecting = slot; const { start, end } = normaliseRangeStartAndEnd(this.dragSelectOrigin, this.dragSelecting); const range = { start: this.convertDate(start), end: this.convertDate(end), isAllDay: slot.isDaySlot, resources: this.resourcesByIndex(slot.id.resourceIndex), originalEvent: args.originalEvent }; this.viewState.notifySlotSelectionDrag(range); this.cdr.markForCheck(); } } dragHintClasses() { const hintClass = this.dragging.hintClass; let result = []; if (hintClass) { result.push(hintClass); } if (this.eventClass) { const eventClass = this.eventClass(this.dragHintEventStyleArgs()); result = result.concat(convertNgClassBindings(eventClass)); } return result; } dragHintEventStyleArgs() { return { event: this.dragging.task.event, resources: this.dragging.resourceItems }; } draggedIsAllDay(task, _slot) { return Boolean(task.event.isAllDay); } dragResourceColor(task, slotResources) { if (this.groupedResources.length) { const index = this.resources.indexOf(this.groupedResources[0]); return getField(slotResources[index], this.groupedResources[0].colorField); } else if (this.resources && this.resources.length) { return task.resources[0].color; } return ''; } resourceValues(task, currentResources) { const result = {}; for (let idx = 0; idx < currentResources.length; idx++) { const resource = this.resources[idx]; const resourceItem = currentResources[idx]; let value; if (Array.isArray(resourceItem)) { // not grouped multiple resource value = getField(task.event, resource.field); } else { value = getField(currentResources[idx], resource.valueField); if (resource.multiple) { const startValue = getField(this.dragging.startResources[idx], resource.valueField); if (startValue !== value) { value = [value]; } else { value = getField(task.event.dataItem, resource.field); } } } setField(result, resource.field, value); } return result; } initResize(args) { args.originalEvent.preventDefault(); const target = args.originalEvent.target; const resizing = this.targetTask(target); if (this.emitEvent('resizeStart', { event: resizing.task.event, dataItem: resizing.task.event.dataItem })) { return; } this.resizing = resizing; resizing.start = resizing.task.start.toUTCDate(); resizing.end = resizing.task.end.toUTCDate(); if (hasClasses(target, 'k-resize-n')) { resizing.direction = 'n'; } else if (hasClasses(target, 'k-resize-s')) { resizing.direction = 's'; } else if (hasClasses(target, 'k-resize-w')) { resizing.direction = 'w'; } else { resizing.direction = 'e'; } this.updateDragContainer(args); const { x, y } = this.coordinatesOffset(args.pageX, args.pageY); resizing.slot = this.slotByPosition(Math.ceil(x), Math.ceil(y), this.container); resizing.offset = { start: resizing.task.start.toUTCDate().getTime() - resizing.slot.start.getTime(), end: resizing.task.end.toUTCDate().getTime() - resizing.slot.start.getTime() }; } resize(args) { const { x, y } = this.coordinatesOffset(args.pageX, args.pageY); const resizing = this.resizing; const { direction, task, offset } = resizing; const slot = this.slotService.groupSlotByPosition(resizing.slot, x, y); if (!slot || slot === resizing.slot) { return; } this.removeSlotClass(); const { start, end, ranges } = this.slotService.resizeRanges(slot, task, direction === 'w' || direction === 'n', offset); resizing.hintClass = null; resizing.start = start; resizing.end = end; resizing.slot = slot; if (this.emitEvent('resize', { event: task.event, dataItem: task.event.dataItem, start: this.convertDate(start), end: this.convertDate(end), setHintClass: this.setHintClass, setSlotClass: this.setSlotClass })) { this.updateHintClass(); return; } this.updateResizeHints(ranges, start, end); this.updateHintContainer(); } updateResizeHints(ranges, _start, _end) { const resizing = this.resizing; const direction = resizing.direction; const horizontal = direction === 'w' || direction === 'e'; const resizeStart = direction === 'w' || direction === 'n'; this.resizeHints = []; for (let idx = 0; idx < ranges.length; idx++) { const range = ranges[idx]; const firstSlot = range[0]; const lastSlot = range[range.length - 1]; const first = idx === 0; const last = idx === ranges.length - 1; this.resizeHints.push({ first: first, last: last, rect: { left: firstSlot.rect.left, top: firstSlot.top, height: horizontal ? firstSlot.height : Math.abs(lastSlot.rect.top - firstSlot.rect.top) + lastSlot.rect.height, width: horizontal ? Math.abs(lastSlot.rect.left - firstSlot.rect.left) + lastSlot.rect.width : firstSlot.width }, start: first && !resizeStart ? resizing.start : firstSlot.start, end: last && resizeStart ? resizing.end : lastSlot.end, class: resizing.hintClass }); } } coordinatesOffset(x, y, container = this.container, offset = this.containerOffset) { const position = x - offset.left + container.scrollLeft; return { x: !this.localization.rtl ? position : this.slotService.containerSize - position, y: y - offset.top + container.scrollTop }; } scrollContainer(callback, args) { clearInterval(this.scrollInterval); const container = this.container; if (!container) { return; } const viewPortY = args.pageY - this.containerOffset.top; const pointerYDistance = Math.abs(container.offsetHeight - viewPortY); const deltaY = args.pageY - this.dragArgs.pageY; const viewPortX = args.pageX - this.containerOffset.left; const pointerXDistance = Math.abs(container.offsetWidth - viewPortX); const deltaX = args.pageX - this.dragArgs.pageX; let scroll = false; let leftChange = 0; let topChange = 0; if (pointerYDistance < MIN_DISTANCE && container.scrollTop + container.offsetHeight < container.scrollHeight && deltaY > 0) { scroll = true; topChange = SCROLL_CHANGE; this.container.scrollTop += MIN_DISTANCE - pointerYDistance; } else if (viewPortY < MIN_DISTANCE && container.scrollTop > 0 && deltaY < 0) { scroll = true; topChange = -SCROLL_CHANGE; this.container.scrollTop -= MIN_DISTANCE - viewPortY; } if (pointerXDistance < MIN_DISTANCE && container.scrollLeft + container.offsetWidth < container.scrollWidth && deltaX > 0) { scroll = true; leftChange = SCROLL_CHANGE; this.container.scrollLeft += MIN_DISTANCE - pointerXDistance; } else if (viewPortX < MIN_DISTANCE && container.scrollLeft > 0 && deltaY < 0) { scroll = true; leftChange = -SCROLL_CHANGE; this.container.scrollLeft -= MIN_DISTANCE - viewPortX; } if (scroll) { this.scrollInterval = setInterval(() => { if (this.container) { this.container.scrollLeft += leftChange; this.container.scrollTop += topChange; callback.call(this, args); } else { clearInterval(this.scrollInterval); } }, SCROLL_INTERVAL); } } emitEvent(name, args) { this.viewState.emitEvent(name, args); return args.prevented; } targetTask(target) { const eventTarget = closestInScope(target, node => node.hasAttribute('data-task-index'), this.element.nativeElement); if (eventTarget) { const index = parseInt(eventTarget.getAttribute('data-task-index'), 10); return { target: eventTarget, task: this.tasks.find(t => t.index === index) }; } } updateHintContainer() { if (this.hintContainer) { this.hintContainer.detectChanges(); } } /** * Converts a "view date" (date stored in the UTC parts of a Date object) to a local date. */ convertDate(date) { return ZonedDate.fromUTCDate(date, this.timezone).toLocalDate(); } onClick(e, eventType = e.type) { this.emitSlotEvent(e, eventType); this.emitTaskEvent(e, eventType); } emitSlotEvent(e, eventType) { const targetSlot = closestInScope(e.target, node => node.hasAttribute('data-slot-index'), this.element.nativeElement); if (targetSlot) { const slotIndex = targetSlot.getAttribute('data-slot-index'); const name = eventType === 'dblclick' ? 'slotDblClick' : 'slotClick'; const slot = this.slotByIndex(slotIndex, e); this.viewState.emitEvent(name, { type: eventType, slot: slot, start: this.convertDate(slot.start), end: this.convertDate(slot.end), isAllDay: slot.isDaySlot, originalEvent: e, resources: this.resources && this.resources.length ? this.resourcesByIndex(slot.id.resourceIndex) : [] }); } } emitTaskEvent(e, eventType) { const targetTask = this.targetTask(e.target); if (targetTask) { const task = targetTask.task; const isSingle = eventType === 'click'; const isDouble = eventType === 'dblclick'; if (isSingle && closestInScope(e.target, node => hasClasses(node, 'k-event-delete'), targetTask.target)) { this.viewState.emitEvent('remove', { event: task.event, dataItem: task.event.dataItem }); } else { const name = isDouble ? 'eventDblClick' : 'eventClick'; this.viewState.emitEvent(name, { type: eventType, event: task.event, originalEvent: e }); targetTask.target.focus(); } } } onKeydown(e) { const targetTask = this.targetTask(e.target); if (targetTask) { const task = targetTask.task; this.viewState.emitEvent('eventKeydown', { event: task.event, dataItem: task.event.dataItem, originalEvent: e }); } } syncTables() { if (this.timesTable) { this.renderer.setStyle(this.timesTable.nativeElement, 'height', `${this.contentTable.nativeElement.offsetHeight}px`); } // Need to explicitly set 'padding-inline-xxx' to 0px when the Scheduler has no height set if (!hasScrollbar(this.content.nativeElement, 'vertical')) { this.renderer.setStyle(this.header.nativeElement, !this.localization.rtl ? 'padding-inline-end' : 'padding-inline-start', '0px'); } this.renderer.removeStyle(this.header.nativeElement, this.localization.rtl ? 'padding-inline-end' : 'padding-inline-start'); if (this.times) { const times = this.times.nativeElement; this.timesHeader.nativeElement.style.width = `${times.offsetWidth}px`; const contentHeight = this.contentHeight === 'auto' ? this.content.nativeElement.offsetHeight : this.contentHeight; this.renderer.setStyle(times, 'height', `${contentHeight - (hasScrollbar(this.content.nativeElement, 'horizontal') ? scrollbarWidth() : 0)}px`); times.scrollTop = this.content.nativeElement.scrollTop; } } updateContentHeight() { const element = this.element.nativeElement; const parent = element.parentNode; const content = this.content.nativeElement; const autoHeight = this.autoHeight || !parent?.style.height; const scrollLeft = content.scrollLeft; const scrollTop = content.scrollTop; this.renderer.setStyle(content, 'height', ''); if (this.times) { this.renderer.setStyle(this.times.nativeElement, 'height', ''); } if (autoHeight) { this.contentHeight = 'auto'; return; } let height = parent.clientHeight; for (let idx = 0; idx < parent.children.length; idx++) { const child = parent.children[idx]; if (child !== element && !ignoreContentChild(child)) { height -= child.offsetHeight; } } height -= this.headerWrap ? this.headerWrap.nativeElement.offsetHeight : 0; this.renderer.setStyle(content, 'height', `${height}px`); this.contentHeight = height; content.scrollLeft = scrollLeft; content.scrollTop = scrollTop; } groupResources() { const resources = this.resources || []; const group = this.group || {}; this.groupedResources = groupResources(group, resources); if (group.orientation !== 'vertical') { this.horizontalResources = this.groupedResources; this.verticalResources = []; } else { this.verticalResources = this.groupedResources; this.horizontalResources = []; } this.spans = this.resourceSpans(); } get taskResources() { if (this.groupedResources.length) { return this.groupedResources; } else if (this.resources && this.resources.length) { return [this.resources[0]]; } else { return [{}]; } } resourceSpans() { const spans = [1]; const resources = this.groupedResources; let span = 1; for (let idx = resources.length - 1; idx > 0; idx--) { span *= ((resources[idx].data || []).length || 1); spans.unshift(span); } return spans; } isInRange(date) { const dateRange = this.dateRange(date); return (!this.min || this.min < dateRange.end) && (!this.max || dateRange.start <= this.max); } createPDFElement() { const contentHeight = this.contentHeight; const scrollTop = this.content.nativeElement.scrollTop; if (contentHeight !== 'auto') { this.autoHeight = true; this.updateView(); } const element = this.element.nativeElement.cloneNode(true); element.style.width = `${this.pdfWidth()}px`; if (contentHeight !== 'auto') { this.autoHeight = false; this.updateView(); } this.pdfService.elementReady.emit({ element: element }); this.content.nativeElement.scrollTop = scrollTop; } pdfWidth() { return this.element.nativeElement.offsetWidth; } containerByPosition({ x, y }) { const content = this.content.nativeElement; const offset = elementOffset(content); if (offset.top <= y && y <= offset.top + offset.height && offset.left <= x && x <= offset.left + offset.width) { return { element: content, offset: offset }; } } execute(e) { if (e.name === 'slotByPosition') { const container = this.containerByPosition(e.args); if (container) { const offset = this.coordinatesOffset(e.args.x, e.args.y, container.element, container.offset); const slot = this.slotByPosition(offset.x, offset.y, container.element); e.result(this.slotFields(slot)); } } else if (e.name === 'eventFromElement') { const target = this.targetTask(e.args.element); if (target) { e.result(target.task.event); } } } slotFields(slot) { return { element: slot.element, resources: this.groupedResources.length ? this.resourcesByIndex(slot.id.resourceIndex) : [], start: this.convertDate(slot.start), end: this.convertDate(slot.end) }; } onStateOptionsChange(changes) { if (changes?.showFooter || changes?.showToolbar) { this.zone.onStable.pipe(take(1)).subscribe(() => this.updateView()); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BaseView, deps: [{ token: i1.ViewContextService }, { token: i2.ViewStateService }, { token: i3.IntlService }, { token: i4.BaseSlotService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i5.PDFService }, { token: i6.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i7.ScrollbarWidthService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: BaseView, inputs: { eventTemplate: "eventTemplate", groupHeaderTemplate: "groupHeaderTemplate", selectedDateFormat: "selectedDateFormat", selectedShortDateFormat: "selectedShortDateFormat", eventHeight: "eventHeight", showToolbar: "showToolbar", showFooter: "showFooter", slotClass: "slotClass", eventClass: "eventClass", eventStyles: "eventStyles", weekStart: "weekStart" }, viewQueries: [{ propertyName: "content", first: true, predicate: ["content"], descendants: true }, { propertyName: "header", first: true, predicate: ["header"], descendants: true }, { propertyName: "contentTable", first: true, predicate: ["contentTable"], descendants: true }, { propertyName: "times", first: true, predicate: ["times"], descendants: true }, { propertyName: "timesHeader", first: true, predicate: ["timesHeader"], descendants: true }, { propertyName: "timesTable", first: true, predicate: ["timesTable"], descendants: true }, { propertyName: "headerWrap", first: true, predicate: ["headerWrap"], descendants: true }, { propertyName: "hintContainer", first: true, predicate: ["hintContainer"], descendants: true }], usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BaseView, decorators: [{ type: Directive }], ctorParameters: () => [{ type: i1.ViewContextService }, { type: i2.ViewStateService }, { type: i3.IntlService }, { type: i4.BaseSlotService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i5.PDFService }, { type: i6.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i7.ScrollbarWidthService }], propDecorators: { eventTemplate: [{ type: Input }], groupHeaderTemplate: [{ type: Input }], selectedDateFormat: [{ type: Input }], selectedShortDateFormat: [{ type: Input }], eventHeight: [{ type: Input }], showToolbar: [{ type: Input }], showFooter: [{ type: Input }], slotClass: [{ type: Input }], eventClass: [{ type: Input }], eventStyles: [{ type: Input }], weekStart: [{ type: Input }], content: [{ type: ViewChild, args: ['content', { static: false }] }], header: [{ type: ViewChild, args: ['header', { static: false }] }], contentTable: [{ type: ViewChild, args: ['contentTable', { static: false }] }], times: [{ type: ViewChild, args: ['times', { static: false }] }], timesHeader: [{ type: ViewChild, args: ['timesHeader', { static: false }] }], timesTable: [{ type: ViewChild, args: ['timesTable', { static: false }] }], headerWrap: [{ type: ViewChild, args: ['headerWrap', { static: false }] }], hintContainer: [{ type: ViewChild, args: ['hintContainer', { static: false }] }] } });