UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

564 lines (563 loc) • 26 kB
/** * DevExtreme (esm/ui/scheduler/appointment_model.js) * Version: 21.1.4 * Build date: Mon Jun 21 2021 * * Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import config from "../../core/config"; import { map, each } from "../../core/utils/iterator"; import dateSerialization from "../../core/utils/date_serialization"; import { getRecurrenceProcessor } from "./recurrence"; import dateUtils from "../../core/utils/date"; import { equalByValue } from "../../core/utils/common"; import { isFunction, isDefined, isString } from "../../core/utils/type"; import { inArray, wrapToArray } from "../../core/utils/array"; import { extend } from "../../core/utils/extend"; import query from "../../data/query"; import { Deferred } from "../../core/utils/deferred"; var toMs = dateUtils.dateToMilliseconds; var DATE_FILTER_POSITION = 0; var USER_FILTER_POSITION = 1; class FilterMaker { constructor(dataAccessors) { this._filterRegistry = null; this._dataAccessors = dataAccessors } isRegistered() { return !!this._filterRegistry } clearRegistry() { delete this._filterRegistry } make(type, args) { if (!this._filterRegistry) { this._filterRegistry = {} } this._make(type).apply(this, args) } _make(type) { switch (type) { case "date": return (min, max, useAccessors) => { var startDate = useAccessors ? this._dataAccessors.getter.startDate : this._dataAccessors.expr.startDateExpr; var endDate = useAccessors ? this._dataAccessors.getter.endDate : this._dataAccessors.expr.endDateExpr; var recurrenceRule = this._dataAccessors.expr.recurrenceRuleExpr; this._filterRegistry.date = [ [ [endDate, ">", min], [startDate, "<", max] ], "or", [recurrenceRule, "startswith", "freq"], "or", [ [endDate, min], [startDate, min] ] ]; if (!recurrenceRule) { this._filterRegistry.date.splice(1, 2) } }; case "user": return userFilter => { this._filterRegistry.user = userFilter } } } combine() { var filter = []; this._filterRegistry.date && filter.push(this._filterRegistry.date); this._filterRegistry.user && filter.push(this._filterRegistry.user); return filter } dateFilter() { return this._filterRegistry.date } } var compareDateWithStartDayHour = (startDate, endDate, startDayHour, allDay, severalDays) => { var startTime = dateUtils.dateTimeFromDecimal(startDayHour); var result = startDate.getHours() >= startTime.hours && startDate.getMinutes() >= startTime.minutes || endDate.getHours() === startTime.hours && endDate.getMinutes() > startTime.minutes || endDate.getHours() > startTime.hours || severalDays || allDay; return result }; var compareDateWithEndDayHour = options => { var { startDate: startDate, endDate: endDate, startDayHour: startDayHour, endDayHour: endDayHour, viewStartDayHour: viewStartDayHour, viewEndDayHour: viewEndDayHour, allDay: allDay, severalDays: severalDays, min: min, max: max, checkIntersectViewport: checkIntersectViewport } = options; var hiddenInterval = (24 - viewEndDayHour + viewStartDayHour) * toMs("hour"); var apptDuration = endDate.getTime() - startDate.getTime(); var delta = (hiddenInterval - apptDuration) / toMs("hour"); var apptStartHour = startDate.getHours(); var apptStartMinutes = startDate.getMinutes(); var result; var endTime = dateUtils.dateTimeFromDecimal(endDayHour); var startTime = dateUtils.dateTimeFromDecimal(startDayHour); var apptIntersectViewport = startDate < max && endDate > min; result = checkIntersectViewport && apptIntersectViewport || apptStartHour < endTime.hours || apptStartHour === endTime.hours && apptStartMinutes < endTime.minutes || allDay && startDate <= max || severalDays && apptIntersectViewport && (apptStartHour < endTime.hours || 60 * endDate.getHours() + endDate.getMinutes() > 60 * startTime.hours); if (apptDuration < hiddenInterval) { if (apptStartHour > endTime.hours && apptStartMinutes > endTime.minutes && delta <= apptStartHour - endDayHour) { result = false } } return result }; class AppointmentModel { constructor(dataSource, dataAccessors, baseAppointmentDuration) { this.setDataAccessors(dataAccessors); this.setDataSource(dataSource); this._updatedAppointmentKeys = []; this._filterMaker = new FilterMaker(dataAccessors); this._baseAppointmentDuration = baseAppointmentDuration } get keyName() { var store = this._dataSource.store(); return store.key() } _createFilter(min, max, remoteFiltering, dateSerializationFormat) { this._filterMaker.make("date", [min, max]); var userFilterPosition = this._excessFiltering() ? this._dataSource.filter()[USER_FILTER_POSITION] : this._dataSource.filter(); this._filterMaker.make("user", [userFilterPosition]); if (remoteFiltering) { this._dataSource.filter(this._combineRemoteFilter(dateSerializationFormat)) } } _excessFiltering() { var dateFilter = this._filterMaker.dateFilter(); var dataSourceFilter = this._dataSource.filter(); return dataSourceFilter && (equalByValue(dataSourceFilter, dateFilter) || dataSourceFilter.length && equalByValue(dataSourceFilter[DATE_FILTER_POSITION], dateFilter)) } _combineFilter() { return this._filterMaker.combine() } _getStoreKey(target) { var store = this._dataSource.store(); return store.keyOf(target) } _filterAppointmentByResources(appointment, resources) { var checkAppointmentResourceValues = (resourceName, resourceIndex) => { var resourceGetter = this._dataAccessors.getter.resources[resourceName]; var resource; if (isFunction(resourceGetter)) { resource = resourceGetter(appointment) } var appointmentResourceValues = wrapToArray(resource); var resourceData = map(resources[resourceIndex].items, item => item.id); for (var j = 0; j < appointmentResourceValues.length; j++) { if (inArray(appointmentResourceValues[j], resourceData) > -1) { return true } } return false }; var result = false; for (var i = 0; i < resources.length; i++) { var resourceName = resources[i].name; result = checkAppointmentResourceValues(resourceName, i); if (!result) { return false } } return result } _filterAppointmentByRRule(appointment, min, max, startDayHour, endDayHour, firstDayOfWeek) { var recurrenceRule = appointment.recurrenceRule; var recurrenceException = appointment.recurrenceException; var allDay = appointment.allDay; var result = true; var appointmentStartDate = appointment.startDate; var appointmentEndDate = appointment.endDate; var recurrenceProcessor = getRecurrenceProcessor(); if (allDay || this._appointmentPartInInterval(appointmentStartDate, appointmentEndDate, startDayHour, endDayHour)) { var trimmedDates = this._trimDates(min, max); min = trimmedDates.min; max = new Date(trimmedDates.max.getTime() - toMs("minute")) } if (recurrenceRule && !recurrenceProcessor.isValidRecurrenceRule(recurrenceRule)) { result = appointmentEndDate > min && appointmentStartDate <= max } if (result && recurrenceProcessor.isValidRecurrenceRule(recurrenceRule)) { result = recurrenceProcessor.hasRecurrence({ rule: recurrenceRule, exception: recurrenceException, start: appointmentStartDate, end: appointmentEndDate, min: min, max: max, firstDayOfWeek: firstDayOfWeek }) } return result } _appointmentPartInInterval(startDate, endDate, startDayHour, endDayHour) { var apptStartDayHour = startDate.getHours(); var apptEndDayHour = endDate.getHours(); return apptStartDayHour <= startDayHour && apptEndDayHour <= endDayHour && apptEndDayHour >= startDayHour || apptEndDayHour >= endDayHour && apptStartDayHour <= endDayHour && apptStartDayHour >= startDayHour } _createAllDayAppointmentFilter(filterOptions) { var { viewStartDayHour: viewStartDayHour, viewEndDayHour: viewEndDayHour } = filterOptions; var that = this; return [ [appointment => that.appointmentTakesAllDay(appointment, viewStartDayHour, viewEndDayHour)] ] } _createCombinedFilter(filterOptions, timeZoneCalculator) { var dataAccessors = this._dataAccessors; var min = new Date(filterOptions.min); var max = new Date(filterOptions.max); var getRecurrenceException = filterOptions.recurrenceException; var { startDayHour: startDayHour, endDayHour: endDayHour, viewStartDayHour: viewStartDayHour, viewEndDayHour: viewEndDayHour, resources: resources, firstDayOfWeek: firstDayOfWeek, checkIntersectViewport: checkIntersectViewport } = filterOptions; var that = this; return [ [appointment => { var result = true; var startDate = new Date(dataAccessors.getter.startDate(appointment)); var endDate = new Date(dataAccessors.getter.endDate(appointment)); var appointmentTakesAllDay = that.appointmentTakesAllDay(appointment, viewStartDayHour, viewEndDayHour); var appointmentTakesSeveralDays = that.appointmentTakesSeveralDays(appointment); var isAllDay = dataAccessors.getter.allDay(appointment); var appointmentIsLong = appointmentTakesSeveralDays || appointmentTakesAllDay; var useRecurrence = isDefined(dataAccessors.getter.recurrenceRule); var recurrenceRule; if (useRecurrence) { recurrenceRule = dataAccessors.getter.recurrenceRule(appointment) } if (resources && resources.length) { result = that._filterAppointmentByResources(appointment, resources) } if (appointmentTakesAllDay && false === filterOptions.allDay) { result = false } var startDateTimeZone = dataAccessors.getter.startDateTimeZone(appointment); var endDateTimeZone = dataAccessors.getter.endDateTimeZone(appointment); var comparableStartDate = timeZoneCalculator.createDate(startDate, { appointmentTimeZone: startDateTimeZone, path: "toGrid" }); var comparableEndDate = timeZoneCalculator.createDate(endDate, { appointmentTimeZone: endDateTimeZone, path: "toGrid" }); if (result && useRecurrence) { var recurrenceException = getRecurrenceException ? getRecurrenceException(appointment) : dataAccessors.getter.recurrenceException(appointment); result = that._filterAppointmentByRRule({ startDate: comparableStartDate, endDate: comparableEndDate, recurrenceRule: recurrenceRule, recurrenceException: recurrenceException, allDay: appointmentTakesAllDay }, min, max, startDayHour, endDayHour, firstDayOfWeek) } if (result && comparableEndDate < min && appointmentIsLong && !isAllDay && (!useRecurrence || useRecurrence && !recurrenceRule)) { result = false } if (result && isDefined(startDayHour) && (!useRecurrence || !filterOptions.isVirtualScrolling)) { result = compareDateWithStartDayHour(comparableStartDate, comparableEndDate, startDayHour, appointmentTakesAllDay, appointmentTakesSeveralDays) } if (result && isDefined(endDayHour)) { result = compareDateWithEndDayHour({ startDate: comparableStartDate, endDate: comparableEndDate, startDayHour: startDayHour, endDayHour: endDayHour, viewStartDayHour: viewStartDayHour, viewEndDayHour: viewEndDayHour, allDay: appointmentTakesAllDay, severalDays: appointmentTakesSeveralDays, min: min, max: max, checkIntersectViewport: checkIntersectViewport }) } if (result && useRecurrence && !recurrenceRule) { if (comparableEndDate < min && !isAllDay) { result = false } } return result }] ] } setDataSource(dataSource) { this._dataSource = dataSource; this.cleanModelState(); this._initStoreChangeHandlers(); this._filterMaker && this._filterMaker.clearRegistry() } _initStoreChangeHandlers() { var dataSource = this._dataSource; var store = null === dataSource || void 0 === dataSource ? void 0 : dataSource.store(); if (store) { store.on("updating", newItem => { this._updatedAppointment = newItem }); store.on("push", pushItems => { var items = dataSource.items(); var keyName = store.key(); pushItems.forEach(pushItem => { var itemExists = 0 !== items.filter(item => item[keyName] === pushItem.key).length; if (itemExists) { this._updatedAppointmentKeys.push({ key: keyName, value: pushItem.key }) } else { var { data: data } = pushItem; data && items.push(data) } }); dataSource.load() }) } } getUpdatedAppointment() { return this._updatedAppointment } getUpdatedAppointmentKeys() { return this._updatedAppointmentKeys } cleanModelState() { this._updatedAppointment = null; this._updatedAppointmentKeys = [] } setDataAccessors(dataAccessors) { this._dataAccessors = dataAccessors; this._filterMaker = new FilterMaker(dataAccessors) } filterByDate(min, max, remoteFiltering, dateSerializationFormat) { if (!this._dataSource) { return } var trimmedDates = this._trimDates(min, max); if (!this._filterMaker.isRegistered()) { this._createFilter(trimmedDates.min, trimmedDates.max, remoteFiltering, dateSerializationFormat) } else { var _this$_dataSource$fil; this._filterMaker.make("date", [trimmedDates.min, trimmedDates.max]); if ((null === (_this$_dataSource$fil = this._dataSource.filter()) || void 0 === _this$_dataSource$fil ? void 0 : _this$_dataSource$fil.length) > 1) { var userFilter = this._serializeRemoteFilter([this._dataSource.filter()[1]], dateSerializationFormat); this._filterMaker.make("user", userFilter) } if (remoteFiltering) { this._dataSource.filter(this._combineRemoteFilter(dateSerializationFormat)) } } } _combineRemoteFilter(dateSerializationFormat) { var combinedFilter = this._filterMaker.combine(); return this._serializeRemoteFilter(combinedFilter, dateSerializationFormat) } _serializeRemoteFilter(filter, dateSerializationFormat) { if (!Array.isArray(filter)) { return filter } filter = extend([], filter); var startDate = this._dataAccessors.expr.startDateExpr; var endDate = this._dataAccessors.expr.endDateExpr; if (isString(filter[0])) { if (config().forceIsoDateParsing && filter.length > 1) { if (filter[0] === startDate || filter[0] === endDate) { filter[filter.length - 1] = dateSerialization.serializeDate(new Date(filter[filter.length - 1]), dateSerializationFormat) } } } for (var i = 0; i < filter.length; i++) { filter[i] = this._serializeRemoteFilter(filter[i], dateSerializationFormat) } return filter } _createAppointmentFilter(filterOptions, timeZoneCalculator) { var combinedFilter = this._createCombinedFilter(filterOptions, timeZoneCalculator); if (this._filterMaker.isRegistered()) { this._filterMaker.make("user", void 0); var trimmedDates = this._trimDates(filterOptions.min, filterOptions.max); this._filterMaker.make("date", [trimmedDates.min, trimmedDates.max, true]); var dateFilter = this.customizeDateFilter(this._filterMaker.combine(), timeZoneCalculator); combinedFilter.push([dateFilter]) } return combinedFilter } filterLoadedAppointments(filterOption, timeZoneCalculator) { var combinedFilter = this._createAppointmentFilter(filterOption, timeZoneCalculator); return query(this.getPreparedDataItems()).filter(combinedFilter).toArray() } filterAllDayAppointments(filterOption) { var combinedFilter = this._createAllDayAppointmentFilter(filterOption); return query(this.getPreparedDataItems()).filter(combinedFilter).toArray() } getPreparedDataItems() { var _this$_dataSource; var dataItems = null === (_this$_dataSource = this._dataSource) || void 0 === _this$_dataSource ? void 0 : _this$_dataSource.items(); if (!dataItems) { return [] } return map(dataItems, item => { var startDate = new Date(this._dataAccessors.getter.startDate(item)); var endDate = new Date(this._dataAccessors.getter.endDate(item)); this.replaceWrongEndDate(item, startDate, endDate); return item }) } replaceWrongEndDate(appointment, startDate, endDate) { if (this._isEndDateWrong(startDate, endDate)) { var isAllDay = this._dataAccessors.getter.allDay(appointment); var calculatedEndDate = this._calculateAppointmentEndDate(isAllDay, startDate); this._dataAccessors.setter.endDate(appointment, calculatedEndDate) } } filterLoadedVirtualAppointments(filterOptions, timeZoneCalculator, groupCount) { var combinedFilters = []; var itemsToFilter = this.getPreparedDataItems(); var needPreFilter = groupCount > 0; if (needPreFilter) { itemsToFilter = itemsToFilter.filter(item => { for (var i = 0; i < filterOptions.length; ++i) { var { resources: resources } = filterOptions[i]; if (this._filterAppointmentByResources(item, resources)) { return true } } }) } filterOptions.forEach(filterOption => { combinedFilters.length && combinedFilters.push("or"); var filter = this._createAppointmentFilter(filterOption, timeZoneCalculator); combinedFilters.push(filter) }); return query(itemsToFilter).filter(combinedFilters).toArray() } _trimDates(min, max) { var minCopy = dateUtils.trimTime(new Date(min)); var maxCopy = dateUtils.trimTime(new Date(max)); maxCopy.setDate(maxCopy.getDate() + 1); return { min: minCopy, max: maxCopy } } hasAllDayAppointments(items, startDayHour, endDayHour) { if (!items) { return false } var that = this; var result = false; each(items, (index, item) => { if (that.appointmentTakesAllDay(item, startDayHour, endDayHour)) { result = true; return false } }); return result } appointmentTakesAllDay(appointment, startDayHour, endDayHour) { var dataAccessors = this._dataAccessors; var startDate = dataAccessors.getter.startDate(appointment); var endDate = dataAccessors.getter.endDate(appointment); var allDay = dataAccessors.getter.allDay(appointment); return allDay || this._appointmentHasAllDayDuration(startDate, endDate, startDayHour, endDayHour) } _appointmentHasAllDayDuration(startDate, endDate, startDayHour, endDayHour) { startDate = new Date(startDate); endDate = new Date(endDate); var appointmentDurationInHours = this._getAppointmentDurationInHours(startDate, endDate); return appointmentDurationInHours >= 24 || this._appointmentHasShortDayDuration(startDate, endDate, startDayHour, endDayHour) } _appointmentHasShortDayDuration(startDate, endDate, startDayHour, endDayHour) { var appointmentDurationInHours = this._getAppointmentDurationInHours(startDate, endDate); var shortDayDurationInHours = endDayHour - startDayHour; return appointmentDurationInHours >= shortDayDurationInHours && startDate.getHours() === startDayHour && endDate.getHours() === endDayHour } _getAppointmentDurationInHours(startDate, endDate) { return (endDate.getTime() - startDate.getTime()) / toMs("hour") } appointmentTakesSeveralDays(appointment) { var dataAccessors = this._dataAccessors; var startDate = new Date(dataAccessors.getter.startDate(appointment)); var endDate = new Date(dataAccessors.getter.endDate(appointment)); return !dateUtils.sameDate(startDate, endDate) } customizeDateFilter(dateFilter, timeZoneCalculator) { var currentFilter = extend(true, [], dateFilter); return (appointment => { var startDate = new Date(this._dataAccessors.getter.startDate(appointment)); var endDate = new Date(this._dataAccessors.getter.endDate(appointment)); appointment = extend(true, {}, appointment); var startDateTimeZone = this._dataAccessors.getter.startDateTimeZone(appointment); var endDateTimeZone = this._dataAccessors.getter.endDateTimeZone(appointment); var comparableStartDate = timeZoneCalculator.createDate(startDate, { appointmentTimeZone: startDateTimeZone, path: "toGrid" }); var comparableEndDate = timeZoneCalculator.createDate(endDate, { appointmentTimeZone: endDateTimeZone, path: "toGrid" }); this._dataAccessors.setter.startDate(appointment, comparableStartDate); this._dataAccessors.setter.endDate(appointment, comparableEndDate); return query([appointment]).filter(currentFilter).toArray().length > 0 }).bind(this) } _calculateAppointmentEndDate(isAllDay, startDate) { if (isAllDay) { return dateUtils.setToDayEnd(new Date(startDate)) } return new Date(startDate.getTime() + this._baseAppointmentDuration * toMs("minute")) } _isEndDateWrong(startDate, endDate) { return !endDate || isNaN(endDate.getTime()) || startDate.getTime() > endDate.getTime() } add(rawAppointment) { return this._dataSource.store().insert(rawAppointment).done(() => this._dataSource.load()) } update(target, data) { var key = this._getStoreKey(target); var d = new Deferred; this._dataSource.store().update(key, data).done(result => this._dataSource.load().done(() => d.resolve(result)).fail(d.reject)).fail(d.reject); return d.promise() } remove(rawAppointment) { var key = this._getStoreKey(rawAppointment); return this._dataSource.store().remove(key).done(() => this._dataSource.load()) } } export default AppointmentModel;