@syncfusion/ej2-schedule
Version:
Flexible scheduling library with more built-in features and enhanced customization options similar to outlook and google calendar, allowing the users to plan and manage their appointments with efficient data-binding support.
895 lines (850 loc) • 84.7 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-len */
import { isNullOrUndefined, closest, extend, EventHandler, setStyleAttribute } from '@syncfusion/ej2-base';
import { createElement, prepend, append, addClass, removeClass } from '@syncfusion/ej2-base';
import { DataManager, Query, Predicate } from '@syncfusion/ej2-data';
import { EventFieldsMapping, EventClickArgs, CellClickEventArgs, TdData, SelectEventArgs, InlineClickArgs, CallbackFunction } from '../base/interface';
import { Schedule } from '../base/schedule';
import { ResourcesModel } from '../models/resources-model';
import { generate, getDateFromRecurrenceDateString } from '../../recurrence-editor/date-generator';
import { CalendarType } from '../../common/calendar-util';
import * as util from '../base/util';
import * as cls from '../base/css-constant';
import * as event from '../base/constant';
import { CurrentAction } from '../base/type';
/**
* EventBase for appointment rendering
*/
export class EventBase {
public parent: Schedule;
public slots: number[] = [];
public cssClass: string;
public groupOrder: string[];
public processedData: Record<string, any>[] = [];
private isDoubleTapped: boolean = false;
/**
* Constructor for EventBase
*
* @param {Schedule} parent Accepts the schedule instance
*/
constructor(parent: Schedule) {
this.parent = parent;
}
public processData(events: Record<string, any>[], timeZonePropChanged?: boolean, oldTimezone?: string): Record<string, any>[] {
const start: Date = this.parent.activeView.startDate();
const end: Date = this.parent.activeView.endDate();
const fields: EventFieldsMapping = this.parent.eventFields;
let processed: Record<string, any>[] = [];
let temp: number = 1;
let generateID: boolean = false;
const resourceCollection: ResourcesModel[] = this.parent.resourceBase ? this.parent.resourceBase.resourceCollection : [];
if (events.length > 0 && isNullOrUndefined(events[0][fields.id])) {
generateID = true;
}
for (let event of events) {
if (generateID) {
event[fields.id] = temp++;
}
event = this.updateEventDateTime(event);
if (timeZonePropChanged) {
this.processTimezoneChange(event, oldTimezone);
} else if (!this.parent.isPrinting && !this.parent.uiStateValues.isPreventTimezone) {
event = this.processTimezone(event);
}
for (let level: number = 0; level < resourceCollection.length; level++) {
if (event[resourceCollection[parseInt(level.toString(), 10)].field] === null || event[resourceCollection[parseInt(level.toString(), 10)].field] === 0) {
event[resourceCollection[parseInt(level.toString(), 10)].field] = undefined;
}
}
if (!isNullOrUndefined(event[fields.recurrenceRule]) && event[fields.recurrenceRule] === '') {
event[fields.recurrenceRule] = null;
}
if (!isNullOrUndefined(event[fields.recurrenceRule]) && isNullOrUndefined(event[fields.recurrenceID]) &&
!(this.parent.crudModule && this.parent.crudModule.crudObj.isCrudAction)) {
processed = processed.concat(this.generateOccurrence(event, null, true));
} else {
if (this.parent.crudModule && this.parent.crudModule.crudObj.isCrudAction) {
if (!isNullOrUndefined(event[fields.recurrenceRule]) && isNullOrUndefined(event[fields.recurrenceID])) {
const recurrenceEvent: Record<string, any>[] = this.generateOccurrence(event, null, true);
for (const occurrence of recurrenceEvent) {
const app: Record<string, any>[] = this.parent.eventsProcessed.filter((data: Record<string, Date>) =>
data[fields.startTime].getTime() - (<Date>occurrence[fields.startTime]).getTime() === 0 &&
data[fields.id] === occurrence[fields.id]);
occurrence.Guid = (app.length > 0) ? app[0].Guid : this.generateGuid();
processed.push(occurrence);
}
} else {
const app: Record<string, any>[] = this.parent.eventsProcessed.filter((data: Record<string, any>) =>
data[this.parent.eventFields.id] === event[this.parent.eventFields.id]);
event.Guid = (app.length > 0) ? app[0].Guid : this.generateGuid();
processed.push(event);
}
} else {
event.Guid = this.generateGuid();
processed.push(event);
}
}
}
this.parent.eventsProcessed = [];
const eventData: Record<string, any>[] = processed.filter((data: Record<string, any>) =>
!data[this.parent.eventFields.isBlock]);
this.parent.eventsProcessed = this.filterEvents(start, end, eventData);
if (!this.parent.activeViewOptions.allowOverlap && this.parent.eventsProcessed.length > 0) {
this.processedData = this.parent.eventsProcessed;
const nonOverlapList: Record<string, any>[] = [];
const fields: EventFieldsMapping = this.parent.eventFields;
for (const data of this.parent.eventsProcessed as Record<string, any>[]) {
const overlappingData: Record<string, any> = this.findOverlappingData(data, nonOverlapList);
if (!overlappingData) {
nonOverlapList.push(data);
} else if (!this.parent.eventSettings.sortComparer) {
const dataDuration: number = new Date(data[fields.endTime]).getTime() - new Date(data[fields.startTime]).getTime();
const duplicateDuration: number = new Date(overlappingData[fields.endTime]).getTime() - new Date(overlappingData[fields.startTime]).getTime();
if ((dataDuration > duplicateDuration && data[fields.startTime] === overlappingData[fields.startTime]) || (data[fields.isAllDay] === true)) {
const index: number = nonOverlapList.indexOf(overlappingData);
if (index !== -1) { nonOverlapList.splice(index, 1); }
nonOverlapList.push(data);
}
}
}
this.parent.eventsProcessed = nonOverlapList;
}
const blockData: Record<string, any>[] = processed.filter((data: Record<string, any>) =>
data[this.parent.eventFields.isBlock]);
for (const eventObj of blockData) {
if (eventObj[fields.isAllDay]) {
const isDifferentDate: boolean = util.resetTime(new Date((eventObj[fields.startTime] as Date).getTime())) <
util.resetTime(new Date((eventObj[fields.endTime] as Date).getTime()));
if (!isDifferentDate) {
eventObj[fields.startTime] = util.resetTime(eventObj[fields.startTime] as Date);
eventObj[fields.endTime] = util.addDays(util.resetTime(eventObj[fields.endTime] as Date), 1);
}
}
}
this.parent.blockProcessed = blockData;
return eventData;
}
private findOverlappingData(eventData: Record<string, any>, eventList: Record<string, any>[]): Record<string, any> | undefined {
const isResource: boolean = this.parent.activeViewOptions.group.resources.length > 0;
const resourceCollection: ResourcesModel[] = isResource ? this.parent.resourceBase.resourceCollection : [];
const lastLevelResource: string = isResource ? resourceCollection[resourceCollection.length - 1].field : null;
const fields: EventFieldsMapping = this.parent.eventFields;
const newStartTime: Date = new Date(eventData[fields.startTime]);
const newEndTime: Date = new Date(eventData[fields.endTime]);
for (const existingEvent of eventList) {
if (
newStartTime < existingEvent[fields.endTime] &&
newEndTime > existingEvent[fields.startTime] &&
existingEvent[fields.id] !== eventData[fields.id] &&
(!isResource || isNullOrUndefined(lastLevelResource) ||
this.compareResourceValues(existingEvent[`${lastLevelResource}`], eventData[`${lastLevelResource}`]))
) {
return existingEvent;
}
}
return undefined;
}
private isOverlapRange(eventData: Record<string, any> | Record<string, any>[], currentAction: CurrentAction = null): boolean {
const isResource: boolean = this.parent.activeViewOptions.group.resources.length > 0;
const resourceCollection: ResourcesModel[] = isResource ? this.parent.resourceBase.resourceCollection : [];
const lastLevelResource: string = isResource ? resourceCollection[resourceCollection.length - 1].field : null;
const eventCollection: Record<string, any>[] = Array.isArray(eventData) ? eventData : [eventData];
const fields: EventFieldsMapping = this.parent.eventFields;
const processOverlappingEvents: (data: Record<string, any>) => Record<string, any>[] = (data: Record<string, any>) => {
return this.processedData.filter((x: Record<string, any>) =>
data[fields.startTime] < x[fields.endTime] &&
data[fields.endTime] > x[fields.startTime] &&
x[fields.id] !== data[fields.id] &&
(!isResource || isNullOrUndefined(lastLevelResource) || this.compareResourceValues(x[`${lastLevelResource}`], data[`${lastLevelResource}`]))
);
};
const overlappedEvents: Record<string, any>[] = [];
let isOverlapAlert: boolean = false;
for (const event of eventCollection) {
const dataCol: Record<string, any>[] = !isNullOrUndefined(event[fields.recurrenceRule]) &&
(isNullOrUndefined(event[fields.recurrenceID]) || event[fields.recurrenceID] === event[fields.id]) &&
(isNullOrUndefined(event[fields.recurrenceID]) || currentAction === 'EditSeries')
? this.generateOccurrence(event)
: [event];
for (const data of dataCol) {
const overlappingEvents: Record<string, any>[] = processOverlappingEvents(data);
if (overlappingEvents.length > 0) {
overlappedEvents.push(...overlappingEvents);
}
if (this.findOverlappingData(data, this.parent.eventsProcessed)) {
isOverlapAlert = true;
}
}
}
this.parent.overlapAppointments = overlappedEvents;
return isOverlapAlert;
}
private compareResourceValues(a: string | number | (string | number)[], b: string | number | (string | number)[]): boolean {
type GetValueFunction = (value: string | number | (string | number)[]) => string | number;
const getValue: GetValueFunction = (value: string | number | (string | number)[]) => Array.isArray(value) ? value[0] : value;
return getValue(a) === getValue(b);
}
public checkOverlap(eventData: Record<string, any> | Record<string, any>[]): boolean {
if (!this.parent.activeViewOptions.allowOverlap) {
if (this.isOverlapRange(eventData)) {
this.parent.quickPopup.openValidationError('overlapAlert', eventData);
return true;
}
}
return false;
}
public updateEventDateTime(eventData: Record<string, any>): Record<string, any> {
if (typeof eventData[this.parent.eventFields.startTime] === 'string') {
eventData[this.parent.eventFields.startTime] = util.getDateFromString(eventData[this.parent.eventFields.startTime]);
}
if (typeof eventData[this.parent.eventFields.endTime] === 'string') {
eventData[this.parent.eventFields.endTime] = util.getDateFromString(eventData[this.parent.eventFields.endTime]);
}
return eventData;
}
public getProcessedEvents(eventCollection: Record<string, any>[] = this.parent.eventsData): Record<string, any>[] {
let processed: Record<string, any>[] = [];
for (const event of eventCollection) {
if (!isNullOrUndefined(event[this.parent.eventFields.recurrenceRule]) &&
isNullOrUndefined(event[this.parent.eventFields.recurrenceID])) {
processed = processed.concat(this.generateOccurrence(event));
} else {
processed.push(event);
}
}
return processed;
}
public timezonePropertyChange(oldTimezone: string): void {
const data: Record<string, any>[] = this.parent.eventsData.concat(this.parent.blockData) as Record<string, any>[];
const processed: Record<string, any>[] = this.processData(data, true, oldTimezone);
this.parent.notify(event.dataReady, { processedData: processed });
}
public timezoneConvert(eventData: Record<string, any>): void {
const fields: EventFieldsMapping = this.parent.eventFields;
eventData[fields.startTimezone] = eventData[fields.startTimezone] || eventData[fields.endTimezone];
eventData[fields.endTimezone] = eventData[fields.endTimezone] || eventData[fields.startTimezone];
if (this.parent.timezone) {
const startTz: string = eventData[fields.startTimezone] as string;
const endTz: string = eventData[fields.endTimezone] as string;
eventData[fields.startTime] = this.parent.tzModule.convert(<Date>eventData[fields.startTime], this.parent.timezone, startTz);
eventData[fields.endTime] = this.parent.tzModule.convert(<Date>eventData[fields.endTime], this.parent.timezone, endTz);
}
}
private processTimezoneChange(event: Record<string, any>, oldTimezone: string): void {
const fields: EventFieldsMapping = this.parent.eventFields;
if (event[fields.isAllDay]) {
return;
}
if (oldTimezone && this.parent.timezone) {
event[fields.startTime] = this.parent.tzModule.convert(<Date>event[fields.startTime], oldTimezone, this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.convert(<Date>event[fields.endTime], oldTimezone, this.parent.timezone);
} else if (!oldTimezone && this.parent.timezone) {
event[fields.startTime] = this.parent.tzModule.add(<Date>event[fields.startTime], this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.add(<Date>event[fields.endTime], this.parent.timezone);
} else if (oldTimezone && !this.parent.timezone) {
event[fields.startTime] = this.parent.tzModule.remove(<Date>event[fields.startTime], oldTimezone);
event[fields.endTime] = this.parent.tzModule.remove(<Date>event[fields.endTime], oldTimezone);
}
}
public processTimezone(event: Record<string, any>, isReverse: boolean = false): Record<string, any> {
const fields: EventFieldsMapping = this.parent.eventFields;
if (event[fields.isAllDay]) {
return event;
}
if (event[fields.startTimezone] || event[fields.endTimezone]) {
const startTimezone: string = <string>event[fields.startTimezone] || <string>event[fields.endTimezone];
const endTimezone: string = <string>event[fields.endTimezone] || <string>event[fields.startTimezone];
if (isReverse) {
if (this.parent.timezone) {
event[fields.startTime] = this.parent.tzModule.convert(<Date>event[fields.startTime], startTimezone, this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.convert(<Date>event[fields.endTime], endTimezone, this.parent.timezone);
event[fields.startTime] = this.parent.tzModule.remove(<Date>event[fields.startTime], this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.remove(<Date>event[fields.endTime], this.parent.timezone);
} else {
event[fields.startTime] = this.parent.tzModule.remove(<Date>event[fields.startTime], startTimezone);
event[fields.endTime] = this.parent.tzModule.remove(<Date>event[fields.endTime], endTimezone);
}
} else {
event[fields.startTime] = this.parent.tzModule.add(<Date>event[fields.startTime], startTimezone);
event[fields.endTime] = this.parent.tzModule.add(<Date>event[fields.endTime], endTimezone);
if (this.parent.timezone) {
event[fields.startTime] = this.parent.tzModule.convert(<Date>event[fields.startTime], startTimezone, this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.convert(<Date>event[fields.endTime], endTimezone, this.parent.timezone);
}
}
} else if (this.parent.timezone) {
if (isReverse) {
event[fields.startTime] = this.parent.tzModule.remove(<Date>event[fields.startTime], this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.remove(<Date>event[fields.endTime], this.parent.timezone);
} else {
event[fields.startTime] = this.parent.tzModule.add(<Date>event[fields.startTime], this.parent.timezone);
event[fields.endTime] = this.parent.tzModule.add(<Date>event[fields.endTime], this.parent.timezone);
}
}
return event;
}
public filterBlockEvents(eventObj: Record<string, any>): Record<string, any>[] {
const fields: EventFieldsMapping = this.parent.eventFields;
const eStart: Date = eventObj[fields.startTime] as Date;
const eEnd: Date = eventObj[fields.endTime] as Date;
let resourceData: TdData;
if (this.parent.activeViewOptions.group.resources.length > 0) {
const data: number = this.getGroupIndexFromEvent(eventObj);
resourceData = this.parent.resourceBase.lastResourceLevel[parseInt(data.toString(), 10)];
}
const blockEvents: Record<string, any>[] = <Record<string, any>[]>extend([], this.parent.blockProcessed, null, true);
for (const eventObj of blockEvents) {
if (eventObj[fields.isAllDay]) {
const isDifferentTime: boolean = (eventObj[fields.endTime] as Date).getTime() >
util.resetTime(new Date((eventObj[fields.endTime] as Date).getTime())).getTime();
if (isDifferentTime) {
eventObj[fields.startTime] = util.resetTime(eventObj[fields.startTime] as Date);
eventObj[fields.endTime] = util.addDays(util.resetTime(eventObj[fields.endTime] as Date), 1);
}
}
}
return this.filterEvents(eStart, eEnd, blockEvents, resourceData);
}
public filterEvents(startDate: Date, endDate: Date, appointments: Record<string, any>[] = this.parent.eventsProcessed, resourceTdData?: TdData): Record<string, any>[] {
const predicate: Predicate = this.parent.dataModule.getStartEndQuery(startDate, endDate);
let filter: Record<string, any>[] = new DataManager({ json: appointments }).executeLocal(new Query().where(predicate)) as Record<string, any>[];
if (resourceTdData) {
filter = this.filterEventsByResource(resourceTdData, filter);
}
return this.sortByTime(filter);
}
public filterEventsByRange(eventCollection: Record<string, any>[], startDate?: Date, endDate?: Date): Record<string, any>[] {
let filteredEvents: Record<string, any>[] = [];
if (startDate && endDate) {
filteredEvents = this.filterEvents(startDate, endDate, eventCollection);
} else if (startDate && !endDate) {
filteredEvents = eventCollection.filter((e: Record<string, any>) => e[this.parent.eventFields.startTime] >= startDate);
} else if (!startDate && endDate) {
filteredEvents = eventCollection.filter((e: Record<string, any>) => e[this.parent.eventFields.endTime] <= endDate);
} else {
filteredEvents = eventCollection;
}
return this.sortByTime(filteredEvents);
}
public filterEventsByResource(resourceTdData: TdData, appointments: Record<string, any>[] = this.parent.eventsProcessed): Record<string, any>[] {
const predicate: Record<string, number | string> = {};
const resourceCollection: ResourcesModel[] = this.parent.resourceBase.resourceCollection;
for (let level: number = 0; level < resourceCollection.length; level++) {
predicate[resourceCollection[parseInt(level.toString(), 10)].field] = resourceTdData.groupOrder[parseInt(level.toString(), 10)];
}
const keys: string[] = Object.keys(predicate);
const filteredCollection: Record<string, any>[] = appointments.filter((eventObj: Record<string, any>) => keys.every((key: string) => {
if (eventObj[`${key}`] instanceof Array) {
return (<(string | number)[]>eventObj[`${key}`]).indexOf(predicate[`${key}`]) > -1;
} else {
return eventObj[`${key}`] === predicate[`${key}`];
}
}));
return filteredCollection;
}
public sortByTime(appointmentsCollection: Record<string, any>[]): Record<string, any>[] {
if (this.parent.eventSettings.sortComparer && (typeof(this.parent.eventSettings.sortComparer) === 'function' || typeof(this.parent.eventSettings.sortComparer) === 'string')) {
appointmentsCollection = this.customSorting(appointmentsCollection);
} else {
const fieldMappings: EventFieldsMapping = this.parent.eventFields;
appointmentsCollection.sort((a: Record<string, any>, b: Record<string, any>) => {
const d1: Date = a[fieldMappings.startTime] as Date;
const d2: Date = b[fieldMappings.startTime] as Date;
return d1.getTime() - d2.getTime();
});
}
return appointmentsCollection;
}
public sortByDateTime(appointments: Record<string, any>[]): Record<string, any>[] {
if (this.parent.eventSettings.sortComparer && (typeof(this.parent.eventSettings.sortComparer) === 'function' || typeof(this.parent.eventSettings.sortComparer) === 'string')) {
appointments = this.customSorting(appointments);
} else {
const fieldMapping: EventFieldsMapping = this.parent.eventFields;
appointments.sort((object1: Record<string, any>, object2: Record<string, any>) => {
const d3: Date = object1[fieldMapping.startTime] as Date;
const d4: Date = object2[fieldMapping.startTime] as Date;
const d5: Date = object1[fieldMapping.endTime] as Date;
const d6: Date = object2[fieldMapping.endTime] as Date;
const d1: number = d5.getTime() - d3.getTime();
const d2: number = d6.getTime() - d4.getTime();
return (d3.getTime() - d4.getTime() || d2 - d1);
});
}
return appointments;
}
private customSorting(appointments: Record<string, any>[]): Record<string, any>[] {
if (typeof(this.parent.eventSettings.sortComparer) === 'function') {
return this.parent.eventSettings.sortComparer.call(this.parent, appointments);
} else if (typeof(this.parent.eventSettings.sortComparer) === 'string') {
const splits: string[] = (this.parent.eventSettings.sortComparer as string).split('.');
let sortFn: Function;
if (!isNullOrUndefined(window)) {
sortFn = (window as Record<string, any>)[splits[splits.length - 1]];
}
if (sortFn) {
return sortFn(appointments);
}
}
return appointments;
}
public getSmallestMissingNumber(array: number[]): number {
const large: number = Math.max(...array);
for (let i: number = 0; i < large; i++) {
if (array.indexOf(i) === -1) { return i; }
}
return large + 1;
}
public splitEventByDay(event: Record<string, any>): Record<string, any>[] {
const eventFields: EventFieldsMapping = this.parent.eventFields;
const data: Record<string, any>[] = [];
const eventStartTime: Date = event[eventFields.startTime] as Date;
const eventEndTime: Date = event[eventFields.endTime] as Date;
const isDifferentDate: boolean = util.resetTime(new Date(eventStartTime.getTime())) <
util.resetTime(new Date(eventEndTime.getTime()));
if (isDifferentDate) {
let start: Date = new Date(eventStartTime.getTime());
let end: Date = util.addDays(util.resetTime(new Date(eventStartTime.getTime())), 1);
const endDate: Date = (eventEndTime.getHours() === 0 && eventEndTime.getMinutes() === 0) ?
eventEndTime : util.addDays(eventEndTime, 1);
let index: number = 1;
const eventLength: number = util.getDaysCount(eventStartTime.getTime(), endDate.getTime());
while (end <= eventEndTime && start.getTime() !== end.getTime()) {
const app: Record<string, any> = <Record<string, any>>extend({}, event);
app[eventFields.startTime] = start;
app[eventFields.endTime] = end;
app.data = { index: index, count: eventLength };
app.Guid = this.generateGuid();
app.isSpanned = true;
data.push(app);
start = end;
if ((util.resetTime(new Date(start.getTime())).getTime() === util.resetTime(new Date(eventEndTime.getTime())).getTime())
&& !(end.getTime() === eventEndTime.getTime())) {
end = new Date(start.getTime());
end = new Date(end.setHours(eventEndTime.getHours(), eventEndTime.getMinutes(), eventEndTime.getSeconds()));
} else {
end = util.addDays(util.resetTime(new Date(start.getTime())), 1);
}
index++;
}
} else {
data.push(event);
}
return data;
}
public splitEvent(event: Record<string, any>, dateRender: Date[]): Record<string, any>[] {
const fields: EventFieldsMapping = this.parent.eventFields;
let start: number = util.resetTime(event[fields.startTime]).getTime();
let end: number = util.resetTime(event[fields.endTime]).getTime();
if (util.getDateInMs(event[fields.endTime] as Date) <= 0) {
const temp: number = util.addDays(util.resetTime(event[fields.endTime]), -1).getTime();
end = start > temp ? start : temp;
}
const orgStart: number = start;
const orgEnd: number = end;
const ranges: Record<string, any>[] = [];
if (start !== end) {
if (start < dateRender[0].getTime()) {
start = dateRender[0].getTime();
}
if (end > dateRender[dateRender.length - 1].getTime()) {
end = dateRender[dateRender.length - 1].getTime();
}
let cStart: number = start;
for (let level: number = 0; level < this.slots.length; level++) {
let slot: number[] = <[number]><unknown>this.slots[parseInt(level.toString(), 10)];
if (this.parent.currentView === 'WorkWeek' || this.parent.currentView === 'TimelineWorkWeek'
|| this.parent.activeViewOptions.group.byDate || this.parent.activeViewOptions.showWeekend) {
const slotDates: Date[] = [];
for (const s of slot) {
slotDates.push(new Date(s));
}
const renderedDates: Date[] = this.getRenderedDates(slotDates);
if (!isNullOrUndefined(renderedDates) && renderedDates.length > 0) {
slot = [];
for (const date of renderedDates) {
slot.push(date.getTime());
}
}
}
if (typeof (slot) === 'number') {
const temp: number = slot;
slot = [];
slot.push(temp);
}
const firstSlot: number = <number>slot[0];
cStart = (cStart <= firstSlot && end >= firstSlot) ? firstSlot : cStart;
if (cStart > end || firstSlot > end) {
break;
}
if (!this.parent.activeViewOptions.group.byDate && this.parent.activeViewOptions.showWeekend &&
this.parent.currentView !== 'WorkWeek' && this.parent.currentView !== 'TimelineWorkWeek') {
const startIndex: number = slot.indexOf(cStart);
if (startIndex !== -1) {
let endIndex: number = slot.indexOf(end);
const hasBreak: boolean = endIndex !== -1;
endIndex = hasBreak ? endIndex : slot.length - 1;
const count: number = ((endIndex - startIndex) + 1);
const isLeft: boolean = (slot[parseInt(startIndex.toString(), 10)] !== orgStart);
const isRight: boolean = (slot[parseInt(endIndex.toString(), 10)] !== orgEnd);
ranges.push(this.cloneEventObject(event, slot[parseInt(startIndex.toString(), 10)], slot[parseInt(endIndex.toString(), 10)], count, isLeft, isRight));
if (hasBreak) {
break;
}
}
} else {
if (this.dateInRange(cStart, slot[0], slot[slot.length - 1])) {
const availSlot: number[] = [];
for (let i: number = 0; i < slot.length; i++) {
if (this.dateInRange(<number>slot[parseInt(i.toString(), 10)], orgStart, orgEnd)) {
availSlot.push(slot[parseInt(i.toString(), 10)]);
}
}
if (availSlot.length > 0) {
if (!this.parent.activeViewOptions.group.byDate) {
const isLeft: boolean = (availSlot[0] !== orgStart);
const isRight: boolean = (availSlot[availSlot.length - 1] !== orgEnd);
ranges.push(this.cloneEventObject(
event, availSlot[0], availSlot[availSlot.length - 1], availSlot.length, isLeft, isRight));
} else {
for (const slot of availSlot) {
ranges.push(this.cloneEventObject(event, slot, slot, 1, (slot !== orgStart), (slot !== orgEnd)));
}
}
}
}
}
}
} else {
ranges.push(this.cloneEventObject(event, start, end, 1, false, false));
}
return ranges;
}
public cloneEventObject(event: Record<string, any>, start: number, end: number, count: number, isLeft: boolean, isRight: boolean): Record<string, any> {
const fields: EventFieldsMapping = this.parent.eventFields;
const e: Record<string, any> = extend({}, event, null, true) as Record<string, any>;
const data: Record<string, any> = { count: count, isLeft: isLeft, isRight: isRight };
data[fields.startTime] = event[fields.startTime];
data[fields.endTime] = event[fields.endTime];
e.data = data;
e[fields.startTime] = new Date(start);
e[fields.endTime] = new Date(end);
return e;
}
private dateInRange(date: number, start: number, end: number): boolean {
return start <= date && date <= end;
}
public getSelectedEventElements(target: Element): Element[] {
this.removeSelectedAppointmentClass();
if (this.parent.selectedElements.length <= 0) {
this.parent.selectedElements.push(target);
} else {
const isAlreadySelected: Element[] = this.parent.selectedElements.filter((element: HTMLElement) =>
element.getAttribute('data-guid') === target.getAttribute('data-guid'));
if (isAlreadySelected.length <= 0) {
const elementSelector: string = 'div[data-guid="' + target.getAttribute('data-guid') + '"]';
const focusElements: Element[] = [].slice.call(this.parent.element.querySelectorAll(elementSelector));
for (const element of focusElements) {
this.parent.selectedElements.push(element);
}
} else {
const selectedElements: Element[] = this.parent.selectedElements.filter((element: HTMLElement) =>
element.getAttribute('data-guid') !== target.getAttribute('data-guid'));
this.parent.selectedElements = selectedElements;
}
}
if (target && this.parent.selectedElements.length > 0) {
this.addSelectedAppointments(this.parent.selectedElements, false);
}
return this.parent.selectedElements;
}
public getSelectedEvents(): EventClickArgs {
const eventSelect: Record<string, any>[] = [];
const elementSelect: HTMLElement[] = [];
const selectAppointments: Element[] = [].slice.call(this.parent.element.querySelectorAll('.' + cls.APPOINTMENT_BORDER)) as Element[];
selectAppointments.filter((element: HTMLElement) => {
const isAlreadyAdded: Record<string, any>[] = eventSelect.filter((event: Record<string, any>) => {
return event.Guid === element.getAttribute('data-guid');
});
if (isAlreadyAdded.length === 0) {
eventSelect.push(this.getEventByGuid(element.getAttribute('data-guid')));
}
elementSelect.push(element);
});
return {
event: eventSelect.length > 1 ? eventSelect : eventSelect[0],
element: elementSelect.length > 1 ? elementSelect : elementSelect[0]
} as EventClickArgs;
}
public removeSelectedAppointmentClass(): void {
const selectedAppointments: Element[] = this.getSelectedAppointments();
removeClass(selectedAppointments, cls.APPOINTMENT_BORDER);
if (this.parent.currentView === 'Agenda' || this.parent.currentView === 'MonthAgenda') {
removeClass(selectedAppointments, cls.AGENDA_SELECTED_CELL);
}
}
public addSelectedAppointments(cells: Element[], preventFocus?: boolean): void {
if (this.parent.currentView !== 'MonthAgenda') {
this.parent.removeSelectedClass();
}
addClass(cells, cls.APPOINTMENT_BORDER);
if (cells.length > 0 && !preventFocus) {
(cells[cells.length - 1] as HTMLElement).focus();
}
}
public getSelectedAppointments(): Element[] {
return [].slice.call(this.parent.element.querySelectorAll('.' + cls.APPOINTMENT_BORDER + ',.' + cls.APPOINTMENT_CLASS + ':focus'));
}
public focusElement(isFocused?: boolean): void {
if (this.parent.eventWindow.dialogObject && this.parent.eventWindow.dialogObject.visible) {
return;
}
const activeEle: Element = document.activeElement;
const selectedCell: Element[] = this.parent.getSelectedCells();
if (selectedCell.length > 0 && ((activeEle && (this.parent.element.contains(activeEle) ||
selectedCell.indexOf(activeEle) !== -1)) || isFocused)) {
if (this.parent.keyboardInteractionModule) {
const target: HTMLTableCellElement = ((!isNullOrUndefined(this.parent.activeCellsData) &&
this.parent.activeCellsData.element) || selectedCell[selectedCell.length - 1]) as HTMLTableCellElement;
this.parent.keyboardInteractionModule.selectCells(target instanceof Array, target);
}
return;
}
const selectedAppointments: Element[] = this.getSelectedAppointments();
if (selectedAppointments.length > 0) {
if (this.parent.activeEventData && this.parent.activeEventData.element && selectedAppointments.indexOf(this.parent.activeEventData.element as Element) > -1) {
(this.parent.activeEventData.element as HTMLElement).focus();
return;
}
(selectedAppointments[selectedAppointments.length - 1] as HTMLElement).focus();
return;
}
}
public selectWorkCellByTime(eventsData: Record<string, any>[]): Element {
let target: Element;
if (this.parent.currentView === 'Agenda' || this.parent.currentView === 'MonthAgenda') {
return target;
}
if (eventsData.length > 0) {
const selectedObject: Record<string, any> = eventsData[eventsData.length - 1];
const eventStartTime: Date = <Date>selectedObject[this.parent.eventFields.startTime];
let nearestTime: number;
const isAllDay: boolean = this.isAllDayAppointment(selectedObject);
if (this.parent.currentView === 'Month' || isAllDay || !this.parent.activeViewOptions.timeScale.enable) {
nearestTime = new Date(+eventStartTime).setHours(0, 0, 0, 0);
}
else {
nearestTime = this.findNearestSlot(eventStartTime);
}
let targetArea: Element;
if (isAllDay && ['Day', 'Week', 'WorkWeek'].indexOf(this.parent.currentView) !== -1) {
targetArea = this.parent.getAllDayRow();
} else {
targetArea = this.parent.getContentTable();
}
let queryString: string = '[data-date="' + new Date(nearestTime).getTime() + '"]';
if (!isNullOrUndefined(this.parent.activeViewOptions.group.resources) &&
this.parent.activeViewOptions.group.resources.length > 0) {
queryString += '[data-group-index="' + this.getGroupIndexFromEvent(selectedObject) + '"]';
}
target = targetArea.querySelector(queryString) as Element;
if (target) {
this.parent.activeCellsData = this.parent.getCellDetails(target);
if (this.parent.keyboardInteractionModule) {
this.parent.keyboardInteractionModule.selectCells(false, target as HTMLTableCellElement);
}
return target;
}
}
return target;
}
private findNearestSlot(appointmentTime: Date): number {
const msMajorInterval: number = this.parent.activeViewOptions.timeScale.interval * util.MS_PER_MINUTE;
const msInterval: number = msMajorInterval / this.parent.activeViewOptions.timeScale.slotCount;
const numberOfSlots: number = Math.round(util.MS_PER_DAY / msInterval);
const startTime: Date = new Date(appointmentTime);
startTime.setHours(0, 0, 0, 0);
const slots: Date[] = Array.from({ length: numberOfSlots }, (_: any, i: number) => {
const slotTime: Date = new Date(startTime.getTime() + i * msInterval);
return slotTime;
});
const nearestSlot: Date = slots.reduce((nearest: Date, slot: Date) => {
const difference: number = Math.abs(appointmentTime.getTime() - slot.getTime());
if (!nearest || difference < Math.abs(appointmentTime.getTime() - nearest.getTime())) {
return slot;
}
return nearest;
}, null);
return Math.trunc(nearestSlot.getTime() / 1000) * 1000;
}
public getGroupIndexFromEvent(eventData: Record<string, any>): number {
let levelIndex: number;
let resource: ResourcesModel;
let levelName: string;
let idField: string;
for (let i: number = this.parent.resourceBase.resourceCollection.length - 1; i >= 0; i--) {
const resourceData: Record<string, any> | string | number = eventData[this.parent.resourceBase.resourceCollection[parseInt(i.toString(), 10)].field] as Record<string, any>;
if (!isNullOrUndefined(resourceData)) {
resource = this.parent.resourceBase.resourceCollection[parseInt(i.toString(), 10)];
levelIndex = i;
levelName = resource.name;
idField = resource.field;
break;
}
}
if (isNullOrUndefined(levelName) && isNullOrUndefined(levelIndex)) {
levelName = this.parent.resourceCollection.slice(-1)[0].name;
levelIndex = this.parent.resourceCollection.length - 1;
idField = this.parent.resourceCollection.slice(-1)[0].field;
resource = this.parent.resourceCollection.filter((e: ResourcesModel, index: number) => {
if (e.name === levelName) {
levelIndex = index;
return e;
}
return null;
})[0];
}
const id: number = ((eventData[`${idField}`] instanceof Array) ?
(eventData[`${idField}`] as Record<string, any>)[0] : eventData[`${idField}`]) as number;
if (levelIndex > 0) {
const parentField: string = this.parent.resourceCollection[levelIndex - 1].field;
return this.parent.resourceBase.getIndexFromResourceId(id, levelName, resource, eventData, parentField);
} else {
return this.parent.resourceBase.getIndexFromResourceId(id, levelName, resource);
}
}
public isAllDayAppointment(event: Record<string, any>): boolean {
const fieldMapping: EventFieldsMapping = this.parent.eventFields;
const isAllDay: boolean = event[fieldMapping.isAllDay] as boolean;
const isFullDay: boolean = ((util.getUniversalTime(<Date>event[fieldMapping.endTime]) - util.getUniversalTime(<Date>event[fieldMapping.startTime]))
/ util.MS_PER_DAY) >= 1;
return (isAllDay || (this.parent.eventSettings.spannedEventPlacement !== 'TimeSlot' && isFullDay)) ? true : false;
}
public addEventListener(): void {
this.parent.on(event.documentClick, this.appointmentBorderRemove, this);
}
public removeEventListener(): void {
this.parent.off(event.documentClick, this.appointmentBorderRemove);
}
private appointmentBorderRemove(event: Event & CellClickEventArgs): void {
const element: HTMLElement = event.event.target as HTMLElement;
if (closest(element as Element, '.' + cls.APPOINTMENT_CLASS)) {
if (this.parent.currentView !== 'MonthAgenda') {
this.parent.removeSelectedClass();
}
} else if (!closest(element as Element, '.' + cls.POPUP_OPEN)) {
if (this.parent.uiStateValues.isTapHold && closest(element, '.' + cls.WORK_CELLS_CLASS + ',.' + cls.ALLDAY_CELLS_CLASS)) {
return;
}
this.parent.uiStateValues.isTapHold = false;
this.removeSelectedAppointmentClass();
this.parent.selectedElements = [];
}
}
public wireAppointmentEvents(element: HTMLElement, event?: Record<string, any>, isPreventCrud: boolean = false): void {
const isReadOnly: boolean = (!isNullOrUndefined(event)) ? event[this.parent.eventFields.isReadonly] as boolean : false;
EventHandler.add(element, 'click', this.eventClick, this);
if (!this.parent.isAdaptive && !this.parent.activeViewOptions.readonly && !isReadOnly) {
EventHandler.add(element, 'touchend', this.eventTouchClick, this);
EventHandler.add(element, 'dblclick', this.eventDoubleClick, this);
}
if (!this.parent.activeViewOptions.readonly && !isReadOnly && !isPreventCrud) {
if (this.parent.resizeModule) {
this.parent.resizeModule.wireResizeEvent(element);
}
if (this.parent.dragAndDropModule) {
this.parent.dragAndDropModule.wireDragEvent(element);
}
}
}
private eventTouchClick(e: Event): void {
if (this.parent.uiStateValues.isTouchScroll || this.parent.uiStateValues.isTapHold || this.parent.uiStateValues.action) {
this.parent.uiStateValues.isTouchScroll = this.parent.uiStateValues.isTapHold = false;
return;
}
setTimeout(() => this.isDoubleTapped = false, 250);
e.preventDefault();
if (this.isDoubleTapped) {
this.eventDoubleClick(e);
} else if (!this.isDoubleTapped) {
this.isDoubleTapped = true;
this.eventClick(e as Event & MouseEvent);
}
}
public renderResizeHandler(element: HTMLElement, spanEvent: Record<string, any>, isReadOnly: boolean): void {
if (!this.parent.resizeModule || !this.parent.allowResizing || this.parent.activeViewOptions.readonly || isReadOnly) {
return;
}
for (const resizeEdge of Object.keys(spanEvent)) {
const resizeHandler: HTMLElement = createElement('div', { className: cls.EVENT_RESIZE_CLASS });
switch (resizeEdge) {
case 'isLeft':
if (!spanEvent.isLeft) {
resizeHandler.appendChild(createElement('div', { className: 'e-left-right-resize' }));
addClass([resizeHandler], this.parent.enableRtl ? cls.RIGHT_RESIZE_HANDLER : cls.LEFT_RESIZE_HANDLER);
prepend([resizeHandler], element);
}
break;
case 'isRight':
if (!spanEvent.isRight) {
resizeHandler.appendChild(createElement('div', { className: 'e-left-right-resize' }));
addClass([resizeHandler], this.parent.enableRtl ? cls.LEFT_RESIZE_HANDLER : cls.RIGHT_RESIZE_HANDLER);
append([resizeHandler], element);
}
break;
case 'isTop':
if (!spanEvent.isTop) {
resizeHandler.appendChild(createElement('div', { className: 'e-top-bottom-resize' }));
addClass([resizeHandler], cls.TOP_RESIZE_HANDLER);
prepend([resizeHandler], element);
}
break;
case 'isBottom':
if (!spanEvent.isBottom) {
resizeHandler.appendChild(createElement('div', { className: 'e-top-bottom-resize' }));
addClass([resizeHandler], cls.BOTTOM_RESIZE_HANDLER);
append([resizeHandler], element);
}
break;
}
}
}
private eventClick(eventData: Event & MouseEvent): void {
const target: HTMLElement = eventData.target as HTMLElement;
if (target.classList.contains(cls.DRAG_CLONE_CLASS) || target.classList.contains(cls.RESIZE_CLONE_CLASS) ||
target.classList.contains(cls.INLINE_SUBJECT_CLASS)) {
return;
}
if ((eventData.ctrlKey || eventData.metaKey) && eventData.which === 1 && this.parent.keyboardInteractionModule) {
this.parent.quickPopup.quickPopup.hide();
this.parent.selectedElements = [].slice.call(this.parent.element.querySelectorAll('.' + cls.APPOINTMENT_BORDER)) as Element[];
const target: Element = closest(<Element>eventData.target, '.' + cls.APPOINTMENT_CLASS) as Element;
this.getSelectedEventElements(target);
this.activeEventData(eventData, false);
const selectArgs: SelectEventArgs = {
data: this.parent.activeEventData.event,
element: this.parent.activeEventData.element,
event: eventData, requestType: 'eventSelect'
};
this.parent.trigger(event.select, selectArgs);
const args: EventClickArgs = <EventClickArgs>extend(this.parent.activeEventData, { cancel: false, originalEvent: eventData });
this.parent.trigger(event.eventClick, args);
} else {
this.removeSelectedAppointmentClass();
this.activeEventData(eventData, true);
const selectEventArgs: SelectEventArgs = {
data: this.parent.activeEventData.event,
element: this.parent.activeEventData.element,
event: eventData, requestType: 'eventSelect'
};
this.parent.trigger(event.select, selectEventArgs);
const args: EventClickArgs = <EventClickArgs>extend(this.parent.activeEventData, { cancel: false, originalEvent: eventData });
this.parent.trigger(event.eventClick, args, (eventClickArgs: EventClickArgs) => {
if (eventClickArgs.cancel) {
this.removeSelectedAppointmentClass();
this.parent.selectedElements = [];
if (this.parent.quickPopup) {
this.parent.quickPopup.quickPopupHide();
}
} else {
if (this.parent.currentView === 'Agenda' || this.parent.currentView === 'MonthAgenda') {
addClass([this.parent.activeEventData.element as