UNPKG

@progress/kendo-ui

Version:

This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.

1,640 lines 82.7 kB
//#region ../src/kendo.scheduler.recurrence.js const __meta__ = { id: "scheduler.recurrence", name: "Recurrence", category: "web", depends: [ "dropdownlist", "datepicker", "numerictextbox", "buttongroup", "icons" ], hidden: true }; (function($, undefined) { var kendo = window.kendo, encode = kendo.htmlEncode, timezone = kendo.timezone, Class = kendo.Class, ui = kendo.ui, Widget = ui.Widget, DropDownList = ui.DropDownList, ButtonGroup = ui.ButtonGroup, kendoDate = kendo.date, setTime = kendoDate.setTime, setDayOfWeek = kendoDate.setDayOfWeek, adjustDST = kendoDate.adjustDST, firstDayOfMonth = kendoDate.firstDayOfMonth, getMilliseconds = kendoDate.getMilliseconds, DAYS_IN_LEAPYEAR = [ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 ], DAYS_IN_YEAR = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 ], MONTHS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ], WEEK_DAYS = { 0: "SU", 1: "MO", 2: "TU", 3: "WE", 4: "TH", 5: "FR", 6: "SA" }, WEEK_DAYS_IDX = { "SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6 }, DATE_FORMATS = [ "yyyy-MM-ddTHH:mm:ss.fffzzz", "yyyy-MM-ddTHH:mm:sszzz", "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm", "yyyy-MM-ddTHH", "yyyy-MM-dd", "yyyyMMddTHHmmssfffzzz", "yyyyMMddTHHmmsszzz", "yyyyMMddTHHmmss", "yyyyMMddTHHmm", "yyyyMMddTHH", "yyyyMMdd" ], RULE_NAMES = [ "months", "weeks", "yearDays", "monthDays", "weekDays", "hours", "minutes", "seconds" ], RULE_NAMES_LENGTH = RULE_NAMES.length, RECURRENCE_DATE_FORMAT = "yyyyMMddTHHmmssZ", limitation = { months: function(date, end, rule) { var monthRules = rule.months, months = ruleValues(monthRules, date.getMonth() + 1), changed = false; if (months !== null) { if (months.length) { date.setMonth(months[0] - 1, 1); } else { date.setFullYear(date.getFullYear() + 1, monthRules[0] - 1, 1); } changed = true; } return changed; }, monthDays: function(date, end, rule) { var monthLength, month, days, changed = false, hours = date.getHours(), normalize = function(monthDay) { if (monthDay < 0) { monthDay = monthLength + monthDay + 1; } return monthDay; }; while (date <= end) { month = date.getMonth(); monthLength = getMonthLength(date); days = ruleValues(rule.monthDays, date.getDate(), normalize); if (days === null) { return changed; } changed = true; if (days.length) { date.setMonth(month, days.sort(numberSortPredicate)[0]); adjustDST(date, hours); if (month === date.getMonth()) { break; } } else { date.setMonth(month + 1, 1); } } return changed; }, yearDays: function(date, end, rule) { var year, yearDays, changed = false, hours = date.getHours(), normalize = function(yearDay) { if (yearDay < 0) { yearDay = year + yearDay; } return yearDay; }; while (date < end) { year = leapYear(date) ? 366 : 365; yearDays = ruleValues(rule.yearDays, dayInYear(date), normalize); if (yearDays === null) { return changed; } changed = true; year = date.getFullYear(); if (yearDays.length) { date.setFullYear(year, 0, yearDays.sort(numberSortPredicate)[0]); adjustDST(date, hours); break; } else { date.setFullYear(year + 1, 0, 1); } } return changed; }, weeks: function(date, end, rule) { var weekStart = rule.weekStart, year, weeks, day, changed = false, hours = date.getHours(), normalize = function(week) { if (week < 0) { week = 53 + week; } return week; }; while (date < end) { weeks = ruleValues(rule.weeks, weekInYear(date, weekStart), normalize); if (weeks === null) { return changed; } changed = true; year = date.getFullYear(); if (weeks.length) { day = weeks.sort(numberSortPredicate)[0] * 7 - 1; date.setFullYear(year, 0, day); setDayOfWeek(date, weekStart, -1); adjustDST(date, hours); break; } else { date.setFullYear(year + 1, 0, 1); } } return changed; }, weekDays: function(date, end, rule) { var offsetDate = new Date(date.getTime() + rule.offsets.start); var normalizeDay = offsetDate.getDay() - date.getDay(); var weekDays = rule.weekDays; var weekStart = rule.weekStart; var weekDayRules = ruleWeekValues(weekDays, offsetDate, weekStart); var hours = date.getHours(); var traverseRuleForNextMont = function(rule) { var nextMonthFirstDay = new Date(date.getFullYear(), date.getMonth() + 1, 1); var ruleDay = normalizeDayIndex(rule.day, weekStart); if (nextMonthFirstDay.getDay() <= ruleDay) { if (!weekDayRule || normalizeDayIndex(weekDayRule.day, weekStart) > ruleDay) { weekDayRule = rule; } } }; var weekDayRule, day; if (weekDayRules === null) { return false; } weekDayRule = weekDayRules[0]; if (!weekDayRule) { if (rule.freq === "monthly" && !rule.positions || rule.position > 0) { weekDays.forEach(traverseRuleForNextMont); } if (!weekDayRule) { weekDayRule = weekDays[0]; } setDayOfWeek(date, weekStart); } day = weekDayRule.day; day -= normalizeDay; if (weekDayRule.offset) { while (date <= end && !isInWeek(date, weekDayRule, weekStart)) { if (weekInMonth(date, weekStart) === numberOfWeeks(date, weekStart)) { date.setMonth(date.getMonth() + 1, 1); adjustDST(date, hours); } else { date.setDate(date.getDate() + 7); adjustDST(date, hours); setDayOfWeek(date, weekStart, -1); } } } if (date.getDay() !== day) { setDayOfWeek(date, day); } return true; }, hours: function(date, end, rule) { var hourRules = rule.hours, startTime = rule._startTime, startHours = startTime.getHours(), hours = ruleValues(hourRules, startHours), changed = false; if (hours !== null) { changed = true; date.setHours(startHours); adjustDST(date, startHours); if (hours.length) { hours = hours[0]; date.setHours(hours); } else { hours = date.getHours(); date.setDate(date.getDate() + 1); adjustDST(date, hours); hours = hourRules[0]; date.setHours(hours); adjustDST(date, hours); } if (rule.minutes) { date.setMinutes(0); } startTime.setHours(hours, date.getMinutes()); } return changed; }, minutes: function(date, end, rule) { var minuteRules = rule.minutes, currentMinutes = date.getMinutes(), minutes = ruleValues(minuteRules, currentMinutes), hours = rule._startTime.getHours(), changed = false; if (minutes !== null) { changed = true; if (minutes.length) { minutes = minutes[0]; } else { hours += 1; minutes = minuteRules[0]; } if (rule.seconds) { date.setSeconds(0); } date.setHours(hours, minutes); hours = hours % 24; adjustDST(date, hours); rule._startTime.setHours(hours, minutes, date.getSeconds()); } return changed; }, seconds: function(date, end, rule) { var secondRules = rule.seconds, hours = rule._startTime.getHours(), seconds = ruleValues(secondRules, date.getSeconds()), minutes = date.getMinutes(), changed = false; if (seconds !== null) { changed = true; if (seconds.length) { date.setSeconds(seconds[0]); } else { minutes += 1; date.setMinutes(minutes, secondRules[0]); if (minutes > 59) { minutes = minutes % 60; hours = (hours + 1) % 24; } } rule._startTime.setHours(hours, minutes, date.getSeconds()); } return changed; } }, BaseFrequency = Class.extend({ next: function(date, rule) { var startTime = rule._startTime, day = startTime.getDate(), minutes, seconds; if (rule.seconds) { seconds = date.getSeconds() + 1; date.setSeconds(seconds); startTime.setSeconds(seconds); startTime.setDate(day); } else if (rule.minutes) { minutes = date.getMinutes() + 1; date.setMinutes(minutes); startTime.setMinutes(minutes); startTime.setDate(day); } else { return false; } return true; }, normalize: function(options) { var rule = options.rule; if (options.idx === 4 && rule.hours) { rule._startTime.setHours(0); this._hour(options.date, rule); } }, limit: function(date, end, rule) { var interval = rule.interval, ruleName, firstRule, modified, idx, day; while (date <= end) { modified = firstRule = undefined; day = date.getDate(); for (idx = 0; idx < RULE_NAMES_LENGTH; idx++) { ruleName = RULE_NAMES[idx]; if (rule[ruleName]) { modified = limitation[ruleName](date, end, rule); if (firstRule !== undefined && modified) { break; } else { firstRule = modified; } } if (modified) { this.normalize({ date, rule, day, idx }); } } if ((interval === 1 || !this.interval(rule, date)) && idx === RULE_NAMES_LENGTH) { break; } } }, interval: function(rule, current) { var start = new Date(rule._startPeriod); var date = new Date(current); var hours = current.getHours(); var weekStart = rule.weekStart; var interval = rule.interval; var frequency = rule.freq; var modified = false; var excess = 0; var month = 0; var day = 1; var diff; var startTimeHours; if (frequency === "hourly") { diff = date.getTimezoneOffset() - start.getTimezoneOffset(); startTimeHours = rule._startTime.getHours(); date = date.getTime(); if (hours !== startTimeHours) { date += (startTimeHours - hours) * kendoDate.MS_PER_HOUR; } date -= start; if (diff) { date -= diff * kendoDate.MS_PER_MINUTE; } diff = Math.floor(date / kendoDate.MS_PER_HOUR); excess = intervalExcess(diff, interval); if (excess !== 0) { this._hour(current, rule, excess); modified = true; } } else if (frequency === "daily") { kendoDate.setTime(date, -start, true); diff = Math.round(date / kendoDate.MS_PER_DAY); excess = intervalExcess(diff, interval); if (excess !== 0) { this._date(current, rule, excess); modified = true; } } else if (frequency === "weekly") { excess = this._getNumberOfWeeksBetweenDates(start, current); var normalizedCurrentIndex = normalizeDayIndex(current.getDay(), weekStart); var normalizedStartIndex = normalizeDayIndex(start.getDay(), weekStart); if (normalizedCurrentIndex < normalizedStartIndex) { excess += 1; } excess = intervalExcess(excess, interval); if (excess !== 0) { kendoDate.setDayOfWeek(current, rule.weekStart, -1); current.setDate(current.getDate() + excess * 7); adjustDST(current, hours); modified = true; } } else if (frequency === "monthly") { diff = current.getFullYear() - start.getFullYear(); diff = current.getMonth() - start.getMonth() + diff * 12; excess = intervalExcess(diff, interval); if (excess !== 0) { day = rule._hasRuleValue ? 1 : current.getDate(); current.setFullYear(current.getFullYear(), current.getMonth() + excess, day); adjustDST(current, hours); modified = true; } } else if (frequency === "yearly") { diff = current.getFullYear() - start.getFullYear(); excess = intervalExcess(diff, interval); if (!rule.months) { month = current.getMonth(); } if (!rule.yearDays && !rule.monthDays && !rule.weekDays) { day = current.getDate(); } if (excess !== 0) { current.setFullYear(current.getFullYear() + excess, month, day); adjustDST(current, hours); modified = true; } } return modified; }, _getNumberOfWeeksBetweenDates: function(first, second) { var weeks = (second - first) / 6048e5; var exactWeeks = Math.floor(weeks); if (weeks - exactWeeks > .99) { exactWeeks = Math.round(weeks); } return exactWeeks; }, _hour: function(date, rule, interval) { var startTime = rule._startTime, hours = startTime.getHours(); if (interval) { hours += interval; } date.setHours(hours); hours = hours % 24; startTime.setHours(hours); adjustDST(date, hours); }, _date: function(date, rule, interval) { var hours = date.getHours(); date.setDate(date.getDate() + interval); if (!adjustDST(date, hours)) { this._hour(date, rule); } } }), HourlyFrequency = BaseFrequency.extend({ next: function(date, rule) { if (!BaseFrequency.fn.next(date, rule)) { this._hour(date, rule, 1); } }, normalize: function(options) { var rule = options.rule; if (options.idx === 4) { rule._startTime.setHours(0); this._hour(options.date, rule); } } }), DailyFrequency = BaseFrequency.extend({ next: function(date, rule) { if (!BaseFrequency.fn.next(date, rule)) { this[rule.hours ? "_hour" : "_date"](date, rule, 1); } } }), WeeklyFrequency = DailyFrequency.extend({ setup: function(rule, eventStartDate) { if (!rule.weekDays) { rule.weekDays = [{ day: eventStartDate.getDay(), offset: 0 }]; } } }), MonthlyFrequency = BaseFrequency.extend({ next: function(date, rule) { var day, hours; if (!BaseFrequency.fn.next(date, rule)) { if (rule.hours) { this._hour(date, rule, 1); } else if (rule.monthDays || rule.weekDays || rule.yearDays || rule.weeks) { this._date(date, rule, 1); } else { day = date.getDate(); hours = date.getHours(); date.setMonth(date.getMonth() + 1); adjustDST(date, hours); while (date.getDate() !== day) { date.setDate(day); adjustDST(date, hours); } this._hour(date, rule); } } }, normalize: function(options) { var rule = options.rule, date = options.date, hours = date.getHours(); if (options.idx === 0 && !rule.monthDays && !rule.weekDays) { date.setDate(options.day); adjustDST(date, hours); } else { BaseFrequency.fn.normalize(options); } }, setup: function(rule, eventStartDate, date) { if (!rule.monthDays && !rule.weekDays) { date.setDate(eventStartDate.getDate()); } } }), YearlyFrequency = MonthlyFrequency.extend({ next: function(date, rule) { var day, hours = date.getHours(); if (!BaseFrequency.fn.next(date, rule)) { if (rule.hours) { this._hour(date, rule, 1); } else if (rule.monthDays || rule.weekDays || rule.yearDays || rule.weeks) { this._date(date, rule, 1); } else if (rule.months) { day = date.getDate(); date.setMonth(date.getMonth() + 1); adjustDST(date, hours); while (date.getDate() !== day) { date.setDate(day); adjustDST(date, hours); } this._hour(date, rule); } else { date.setFullYear(date.getFullYear() + 1); adjustDST(date, hours); this._hour(date, rule); } } }, setup: function() {} }), frequencies = { "hourly": new HourlyFrequency(), "daily": new DailyFrequency(), "weekly": new WeeklyFrequency(), "monthly": new MonthlyFrequency(), "yearly": new YearlyFrequency() }, CLICK = "click", CHANGE = "change"; function intervalExcess(diff, interval) { var excess; if (diff !== 0 && diff < interval) { excess = interval - diff; } else { excess = diff % interval; if (excess) { excess = interval - excess; } } return excess; } function dayInYear(date) { var month = date.getMonth(); var days = leapYear(date) ? DAYS_IN_LEAPYEAR[month] : DAYS_IN_YEAR[month]; return days + date.getDate(); } function weekInYear(date, weekStart) { var year, days; date = new Date(date.getFullYear(), date.getMonth(), date.getDate()); adjustDST(date, 0); year = date.getFullYear(); if (weekStart !== undefined) { setDayOfWeek(date, weekStart, -1); date.setDate(date.getDate() + 4); } else { date.setDate(date.getDate() + (4 - (date.getDay() || 7))); } adjustDST(date, 0); days = Math.floor((date.getTime() - new Date(year, 0, 1, -6)) / 864e5); return 1 + Math.floor(days / 7); } function weekInMonth(date, weekStart) { var firstWeekDay = firstDayOfMonth(date).getDay(); var firstWeekLength = 7 - (firstWeekDay + 7 - (weekStart || 7)) || 7; if (firstWeekLength < 0) { firstWeekLength += 7; } return Math.ceil((date.getDate() - firstWeekLength) / 7) + 1; } function normalizeDayIndex(weekDay, weekStart) { return weekDay + (weekDay < weekStart ? 7 : 0); } function normalizeOffset(date, rule, weekStart) { var offset = rule.offset; if (!offset) { return weekInMonth(date, weekStart); } var lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0); var weeksInMonth = weekInMonth(lastDate, weekStart); var day = normalizeDayIndex(rule.day, weekStart); var skipFirst = day < normalizeDayIndex(new Date(date.getFullYear(), date.getMonth(), 1).getDay(), weekStart); var skipLast = day > normalizeDayIndex(lastDate.getDay(), weekStart); if (offset < 0) { offset = weeksInMonth + (offset + 1 - (skipLast ? 1 : 0)); } else if (skipFirst) { offset += 1; } weeksInMonth -= skipLast ? 1 : 0; if (offset < (skipFirst ? 1 : 0) || offset > weeksInMonth) { return null; } return offset; } function numberOfWeeks(date, weekStart) { return weekInMonth(new Date(date.getFullYear(), date.getMonth() + 1, 0), weekStart); } function isInWeek(date, rule, weekStart) { return weekInMonth(date, weekStart) === normalizeOffset(date, rule, weekStart); } function ruleWeekValues(weekDays, date, weekStart) { var currentDay = normalizeDayIndex(date.getDay(), weekStart); var length = weekDays.length; var ruleWeekOffset; var weekDay, day; var weekNumber; var result = []; var idx = 0; for (; idx < length; idx++) { weekDay = weekDays[idx]; weekNumber = weekInMonth(date, weekStart); ruleWeekOffset = normalizeOffset(date, weekDay, weekStart); if (ruleWeekOffset === null) { continue; } if (weekNumber < ruleWeekOffset) { result.push(weekDay); } else if (weekNumber === ruleWeekOffset) { day = normalizeDayIndex(weekDay.day, weekStart); if (currentDay < day) { result.push(weekDay); } else if (currentDay === day) { return null; } } } return result; } function ruleValues(rules, value, normalize) { var idx = 0, length = rules.length, availableRules = [], ruleValue; for (; idx < length; idx++) { ruleValue = rules[idx]; if (normalize) { ruleValue = normalize(ruleValue); } if (value === ruleValue) { return null; } else if (value < ruleValue) { availableRules.push(ruleValue); } } return availableRules; } function parseArray(list, range) { var idx = 0, length = list.length, value; for (; idx < length; idx++) { value = parseInt(list[idx], 10); if (isNaN(value) || value < range.start || value > range.end || value === 0 && range.start < 0) { return null; } list[idx] = value; } return list.sort(numberSortPredicate); } function parseWeekDayList(list) { var idx = 0, length = list.length, value, valueLength, day; for (; idx < length; idx++) { value = list[idx]; valueLength = value.length; day = value.substring(valueLength - 2).toUpperCase(); day = WEEK_DAYS_IDX[day]; if (day === undefined) { return null; } list[idx] = { offset: parseInt(value.substring(0, valueLength - 2), 10) || 0, day }; } return list; } function serializeWeekDayList(list) { var idx = 0, length = list.length, value, valueString, result = []; for (; idx < length; idx++) { value = list[idx]; if (typeof value === "string") { valueString = value; } else { valueString = "" + WEEK_DAYS[value.day]; if (value.offset) { valueString = value.offset + valueString; } } result.push(valueString); } return result.toString(); } function getMonthLength(date) { var month = date.getMonth(); if (month === 1) { if (new Date(date.getFullYear(), 1, 29).getMonth() === 1) { return 29; } return 28; } return MONTHS[month]; } function leapYear(year) { year = year.getFullYear(); return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; } function numberSortPredicate(a, b) { return a - b; } function parseExceptions(exceptions, zone) { var idx = 0, length, date, dates = []; if (exceptions) { exceptions = exceptions.split(exceptions.indexOf(";") !== -1 ? ";" : ","); length = exceptions.length; for (; idx < length; idx++) { date = parseUTCDate(exceptions[idx], zone); if (date) { dates.push(date); } } } return dates; } function isException(exceptions, date, zone) { var dates = Array.isArray(exceptions) ? exceptions : parseExceptions(exceptions, zone), dateTime = date.getTime() - date.getMilliseconds(), idx = 0, length = dates.length; for (; idx < length; idx++) { if (dates[idx].getTime() === dateTime) { return true; } } return false; } function toExceptionString(dates, zone) { var idx = 0; var length; var date; var result = [].concat(dates); for (length = result.length; idx < length; idx++) { date = result[idx]; date = kendo.timezone.convert(date, zone || date.getTimezoneOffset(), "Etc/UTC"); result[idx] = kendo.toString(date, RECURRENCE_DATE_FORMAT); } return result.join(","); } function startPeriodByFreq(start, rule) { var date = new Date(start); switch (rule.freq) { case "yearly": date.setFullYear(date.getFullYear(), 0, 1); break; case "monthly": date.setFullYear(date.getFullYear(), date.getMonth(), 1); break; case "weekly": setDayOfWeek(date, rule.weekStart, -1); break; default: break; } if (rule.hours) { date.setHours(0); } if (rule.minutes) { date.setMinutes(0); } if (rule.seconds) { date.setSeconds(0); } return date; } function endPeriodByFreq(start, rule) { var date = new Date(start); switch (rule.freq) { case "yearly": date.setFullYear(date.getFullYear(), 11, 31); break; case "monthly": date.setFullYear(date.getFullYear(), date.getMonth() + 1, 0); break; case "weekly": setDayOfWeek(date, rule.weekStart, -1); date.setDate(date.getDate() + 6); break; default: break; } if (rule.hours) { date.setHours(23); } if (rule.minutes) { date.setMinutes(59); } if (rule.seconds) { date.setSeconds(59); } return date; } function eventsByPosition(periodEvents, start, positions, until) { var periodEventsLength = periodEvents.length; var events = []; var position; var event; var inPeriod; for (var idx = 0, length = positions.length; idx < length; idx++) { position = positions[idx]; if (position < 0) { position = periodEventsLength + position; } else { position -= 1; } event = periodEvents[position]; inPeriod = until ? event.start < until : true; if (event && event.start >= start && inPeriod) { events.push(event); } } return events; } function removeExceptionDates(periodEvents, exceptionDates, zone) { var events = []; var event; for (var idx = 0; idx < periodEvents.length; idx++) { event = periodEvents[idx]; if (!isException(exceptionDates, event.start, zone)) { events.push(event); } } return events; } function expand(event, start, end, zone) { var rule, startTime, endTime, endDate, hours, minutes, seconds, durationMS, startPeriod, inPeriod, ruleStart, ruleEnd, useEventStart, freqName, exceptionDates, eventStartTime, eventStartMS, eventStart, count, freq, positions, currentIdx, periodEvents, events = [], shiftedStart, shiftedEnd, shiftedStartTime, shifterEndTime; rule = parseRule(event.recurrenceRule, zone, { start: (getZoneOffset(event.start, zone) - getZoneOffset(event.start, event.startTimezone || zone)) * kendo.date.MS_PER_MINUTE, end: (getZoneOffset(event.end, zone) - getZoneOffset(event.end, event.endTimezone || zone)) * kendo.date.MS_PER_MINUTE }); if (!rule) { return [event]; } positions = rule.positions; currentIdx = positions ? 0 : 1; ruleStart = rule.start; ruleEnd = rule.end; if (ruleStart || ruleEnd) { event = event.clone({ start: ruleStart ? new Date(ruleStart.value[0]) : undefined, end: ruleEnd ? new Date(ruleEnd.value[0]) : undefined }); } eventStart = event.start; eventStartMS = eventStart.getTime(); eventStartTime = getMilliseconds(eventStart); exceptionDates = parseExceptions(event.recurrenceException, zone); if (!exceptionDates[0] && rule.exdates) { exceptionDates = rule.exdates.value; event.set("recurrenceException", toExceptionString(exceptionDates, zone)); } startPeriod = start = new Date(start); end = new Date(end); freqName = rule.freq; freq = frequencies[freqName]; count = rule.count; if (rule.until && rule.until < end) { end = new Date(rule.until); } useEventStart = freqName === "yearly" || freqName === "monthly" || freqName === "weekly"; if (start < eventStartMS || count || rule.interval > 1 || useEventStart || (freqName === "daily" || freqName === "hourly") && !rule.seconds) { start = new Date(eventStartMS); } else { hours = start.getHours(); minutes = start.getMinutes(); seconds = start.getSeconds(); if (!rule.hours) { hours = eventStart.getHours(); } if (!rule.minutes) { minutes = eventStart.getMinutes(); } if (!rule.seconds) { seconds = eventStart.getSeconds(); } start.setHours(hours, minutes, seconds, eventStart.getMilliseconds()); } rule._startPeriod = new Date(start); if (positions) { start = startPeriodByFreq(start, rule); end = endPeriodByFreq(end, rule); var diff = getMilliseconds(end) - getMilliseconds(start); if (diff < 0) { hours = start.getHours(); end.setHours(hours, start.getMinutes(), start.getSeconds(), start.getMilliseconds()); kendoDate.adjustDST(end, hours); } rule._startPeriod = new Date(start); rule._endPeriod = endPeriodByFreq(start, rule); } durationMS = event.duration(); var allDayCompensation = event.isAllDay ? 864e5 : 0; rule._startTime = startTime = kendoDate.toInvariantTime(start); if (freq.setup) { freq.setup(rule, eventStart, start); } freq.limit(start, end, rule); while (start <= end) { endDate = new Date(start); setTime(endDate, durationMS); var inPeriodCheckEndDate = new Date(start); setTime(inPeriodCheckEndDate, durationMS + allDayCompensation); inPeriod = start >= startPeriod || inPeriodCheckEndDate > startPeriod; if (inPeriod && !isException(exceptionDates, start, zone) || positions) { startTime = kendoDate.toUtcTime(kendoDate.getDate(start)) + getMilliseconds(rule._startTime); endTime = startTime + durationMS; if (eventStartMS !== start.getTime() || eventStartTime !== getMilliseconds(rule._startTime)) { var startZone = event.startTimezone || event.endTimezone; var endZone = event.endTimezone || event.startTimezone; if (!event.isAllDay) { if (zone && startZone || !zone && !startZone) { var startOffsetDiff = getZoneOffset(start, zone) - getZoneOffset(event.start, zone); var endOffsetDiff = getZoneOffset(endDate, zone) - getZoneOffset(event.end, zone); var startTZOffsetDiff = getZoneOffset(start, startZone) - getZoneOffset(event.start, startZone); var endTZOffsetDiff = getZoneOffset(endDate, endZone) - getZoneOffset(event.end, endZone); if (startOffsetDiff !== startTZOffsetDiff) { var offsetTicksStart = (startOffsetDiff - startTZOffsetDiff) * 6e4; shiftedStart = new Date(start.getTime() - offsetTicksStart); shiftedStartTime = startTime - offsetTicksStart; } if (endOffsetDiff !== endTZOffsetDiff) { var offsetTicksEnd = (endOffsetDiff - endTZOffsetDiff) * 6e4; shiftedEnd = new Date(endDate.getTime() - offsetTicksEnd); shifterEndTime = endTime - offsetTicksEnd; } } } events.push(event.toOccurrence({ start: shiftedStart || new Date(start), end: shiftedEnd || endDate, _startTime: shiftedStartTime || startTime, _endTime: shifterEndTime || endTime })); shiftedStart = shiftedEnd = shiftedStartTime = shifterEndTime = null; } else { event._startTime = startTime; event._endTime = endTime; events.push(event); } } if (positions) { freq.next(start, rule); freq.limit(start, end, rule); if (start > rule._endPeriod) { periodEvents = eventsByPosition(events.slice(currentIdx), eventStart, positions, rule.until); periodEvents = removeExceptionDates(periodEvents, exceptionDates, zone); events = events.slice(0, currentIdx).concat(periodEvents); rule._endPeriod = endPeriodByFreq(start, rule); currentIdx = events.length; } if (count && count === currentIdx) { break; } } else { if (count && count === currentIdx) { break; } currentIdx += 1; var isMissingDSTHour = isDSTMissingHour(start); freq.next(start, rule); if (isMissingDSTHour && rule.freq !== "hourly" && kendoDate.toInvariantTime(event.start).getTime() !== kendoDate.toInvariantTime(start).getTime()) { rule._startTime = startTime = new Date(start.getTime() - 36e5); } freq.limit(start, end, rule); } } return events; } function isDSTMissingHour(date) { var dateOffset = date.getTimezoneOffset(); var dateMinusHour = new Date(date.getTime() - 36e5); var dateMinusHourOffset = dateMinusHour.getTimezoneOffset(); return dateOffset < dateMinusHourOffset; } function getZoneOffset(date, zone) { return zone ? kendo.timezone.offset(date, zone) : date.getTimezoneOffset(); } function parseUTCDate(value, zone) { value = kendo.parseDate(value, DATE_FORMATS); if (value && zone) { value = timezone.apply(value, zone); } return value; } function parseDateRule(dateRule, zone) { var pairs = dateRule.split(";"); var pair; var property; var value; var tzid; var valueIdx, valueLength; for (var idx = 0, length = pairs.length; idx < length; idx++) { pair = pairs[idx].split(":"); property = pair[0]; value = pair[1]; if (property.indexOf("TZID") !== -1) { tzid = property.substring(property.indexOf("TZID")).split("=")[1]; } if (value) { value = value.split(","); for (valueIdx = 0, valueLength = value.length; valueIdx < valueLength; valueIdx++) { value[valueIdx] = parseUTCDate(value[valueIdx], tzid || zone); } } } if (value) { return { value, tzid }; } } function parseRule(recur, zone, offsets) { var instance = {}; var splits, value; var idx = 0, length; var ruleValue = false; var rule, part, parts; var property, weekStart, weekDays; var predicate = function(a, b) { var day1 = a.day, day2 = b.day; if (day1 < weekStart) { day1 += 7; } if (day2 < weekStart) { day2 += 7; } return day1 - day2; }; if (!recur) { return null; } parts = recur.split("\n"); if (!parts[1] && (recur.indexOf("DTSTART") !== -1 || recur.indexOf("DTEND") !== -1 || recur.indexOf("EXDATE") !== -1)) { parts = recur.split(" "); } for (idx = 0, length = parts.length; idx < length; idx++) { part = kendo.trim(parts[idx]); if (part.indexOf("DTSTART") !== -1) { instance.start = parseDateRule(part, zone); } else if (part.indexOf("DTEND") !== -1) { instance.end = parseDateRule(part, zone); } else if (part.indexOf("EXDATE") !== -1) { instance.exdates = parseDateRule(part, zone); } else if (part.indexOf("RRULE") !== -1) { rule = part.substring(6); } else if (kendo.trim(part)) { rule = part; } } rule = rule.split(";"); for (idx = 0, length = rule.length; idx < length; idx++) { property = rule[idx]; splits = property.split("="); value = kendo.trim(splits[1]).split(","); switch (kendo.trim(splits[0]).toUpperCase()) { case "FREQ": instance.freq = value[0].toLowerCase(); break; case "UNTIL": instance.until = parseUTCDate(value[0], zone); break; case "COUNT": instance.count = parseInt(value[0], 10); break; case "INTERVAL": instance.interval = parseInt(value[0], 10); break; case "BYSECOND": instance.seconds = parseArray(value, { start: 0, end: 60 }); ruleValue = true; break; case "BYMINUTE": instance.minutes = parseArray(value, { start: 0, end: 59 }); ruleValue = true; break; case "BYHOUR": instance.hours = parseArray(value, { start: 0, end: 23 }); ruleValue = true; break; case "BYMONTHDAY": instance.monthDays = parseArray(value, { start: -31, end: 31 }); ruleValue = true; break; case "BYYEARDAY": instance.yearDays = parseArray(value, { start: -366, end: 366 }); ruleValue = true; break; case "BYMONTH": instance.months = parseArray(value, { start: 1, end: 12 }); ruleValue = true; break; case "BYDAY": instance.weekDays = weekDays = parseWeekDayList(value); ruleValue = true; break; case "BYWEEKNO": instance.weeks = parseArray(value, { start: -53, end: 53 }); ruleValue = true; break; case "BYSETPOS": instance.positions = parseArray(value, { start: -366, end: 366 }); break; case "WKST": instance.weekStart = weekStart = WEEK_DAYS_IDX[value[0]]; break; } } if (instance.freq === undefined || instance.count !== undefined && instance.until) { return null; } if (!instance.interval) { instance.interval = 1; } if (weekStart === undefined) { instance.weekStart = weekStart = kendo.culture().calendar.firstDay; } if (weekDays) { instance.weekDays = weekDays.sort(predicate); } if (instance.positions && !ruleValue) { instance.positions = null; } instance._hasRuleValue = ruleValue; instance.offsets = offsets; return instance; } function serializeDateRule(dateRule, zone) { var value = dateRule.value; var tzid = dateRule.tzid || ""; var length = value.length; var idx = 0; var val; for (; idx < length; idx++) { val = value[idx]; val = timezone.convert(val, tzid || zone || val.getTimezoneOffset(), "Etc/UTC"); value[idx] = kendo.toString(val, "yyyyMMddTHHmmssZ"); } if (tzid) { tzid = ";TZID=" + tzid; } return tzid + ":" + value.join(",") + " "; } function serialize(rule, zone) { var weekStart = rule.weekStart; var ruleString = "FREQ=" + rule.freq.toUpperCase(); var exdates = rule.exdates || ""; var start = rule.start || ""; var end = rule.end || ""; var until = rule.until; if (weekStart === undefined) { weekStart = kendo.culture().calendar.firstDay; } if (rule.interval > 1) { ruleString += ";INTERVAL=" + rule.interval; } if (rule.count) { ruleString += ";COUNT=" + rule.count; } if (until) { until = timezone.convert(until, zone || until.getTimezoneOffset(), "Etc/UTC"); ruleString += ";UNTIL=" + kendo.toString(until, "yyyyMMddTHHmmssZ"); } if (rule.months) { ruleString += ";BYMONTH=" + rule.months; } if (rule.weeks) { ruleString += ";BYWEEKNO=" + rule.weeks; } if (rule.yearDays) { ruleString += ";BYYEARDAY=" + rule.yearDays; } if (rule.monthDays) { ruleString += ";BYMONTHDAY=" + rule.monthDays; } if (rule.weekDays) { ruleString += ";BYDAY=" + serializeWeekDayList(rule.weekDays); } if (rule.hours) { ruleString += ";BYHOUR=" + rule.hours; } if (rule.minutes) { ruleString += ";BYMINUTE=" + rule.minutes; } if (rule.seconds) { ruleString += ";BYSECOND=" + rule.seconds; } if (rule.positions) { ruleString += ";BYSETPOS=" + rule.positions; } if (weekStart !== undefined) { ruleString += ";WKST=" + WEEK_DAYS[weekStart]; } if (start) { start = "DTSTART" + serializeDateRule(start, zone); } if (end) { end = "DTEND" + serializeDateRule(end, zone); } if (exdates) { exdates = "EXDATE" + serializeDateRule(exdates, zone); } if (start || end || exdates) { ruleString = start + end + exdates + "RRULE:" + ruleString; } return ruleString; } kendo.recurrence = { rule: { parse: parseRule, serialize }, expand, dayInYear, weekInYear, weekInMonth, numberOfWeeks, isException, toExceptionString }; var RECURRENCE_VIEW_TEMPLATE = kendo.template(({ frequency, messages, end }) => (frequency !== "never" ? "<div class=\"k-form-field\">" + `<label id="recur_interval_label" class="k-label k-form-label" for="recur_interval">${encode(messages.repeatEvery)}</label>` + "<div class=\"k-form-field-wrap\">" + `<input id="recur_interval" ref-recur-interval title="${encode(messages.repeatEvery)} ${encode(messages.interval)}" aria-labelledby="recur_interval_label">${encode(messages.interval)}` + "</div>" + "</div>" : "") + (frequency === "weekly" ? "<div class=\"k-form-field\">" + `<label class="k-label k-form-label">${encode(messages.repeatOn)}</label>` + "<div class=\"k-form-field-wrap\">" + `<div class="k-button-group-stretched k-recur-weekday-buttons" title="${encode(messages.repeatOn)}"></div></div>` + "</div>" + "</div>" : "") + (frequency === "monthly" ? `<div class="k-form-field"><label class="k-label k-form-label">${encode(messages.repeatOn)}</label></div>` + "<div class=\"k-form-field-wrap\">" + "<ul class=\"k-radio-list\">" + "<li class=\"k-radio-item\">" + `<label class="k-radio-label"><input class="k-recur-month-radio k-radio" type="radio" name="month" value="monthday" title="${encode(messages.repeatOn)}" />${encode(messages.date)}:</label>` + `<input ref-recur-monthday aria-label="${encode(messages.date)}" title="${encode(messages.date)}" />` + "</li>" + "<li class=\"k-radio-item\">" + `<input class="k-recur-month-radio k-radio" type="radio" name="month" value="weekday" aria-label="${encode(messages.repeatOn)} ${encode(messages.day)}" title="${encode(messages.repeatOn)} ${encode(messages.day)}" />` + `<input class="k-recur-weekday-offset" title="${encode(messages.repeatOn)}" ${kendo.attr("style-width")}="8em" />` + `<input class="k-recur-weekday" title="${encode(messages.day)}" ${kendo.attr("style-width")}="8em" />` + "</li>" + "</ul>" + "</div>" + "</div>" : "") + (frequency === "yearly" ? `<div class="k-form-field"><label class="k-label k-form-label">${encode(messages.repeatOn)}</label>` + "<div class=\"k-form-field-wrap\">" + "<ul class=\"k-radio-list\">" + "<li class=\"k-radio-item\">" + `<input class="k-recur-year-radio k-radio" type="radio" name="year" value="monthday" title="${encode(messages.repeatOn)} ${encode(messages.month)}" />` + `<input class="k-recur-month" aria-label="${encode(messages.month)}" title="${encode(messages.month)}" ${kendo.attr("style-width")}="8em" />` + `<input ref-recur-monthday aria-label="${encode(messages.date)}" title="${encode(messages.date)}" />` + "</li>" + "<li class=\"k-radio-item\">" + `<input class="k-recur-year-radio k-radio" type="radio" name="year" value="weekday" title="${encode(messages.repeatOn)} ${encode(messages.day)}" />` + `<input class="k-recur-weekday-offset" title="${encode(messages.repeatOn)}" ${kendo.attr("style-width")}="8em"/><input class="k-recur-weekday" title="${encode(messages.day)}" ${kendo.attr("style-width")}="8em"/>${encode(messages.of)}<input class="k-recur-month" title="${encode(messages.of + messages.month)}" ${kendo.attr("style-width")}="8em"/>` + "</li>" + "</ul>" + "</div>" + "</div>" : "") + (frequency !== "never" ? `<div class="k-form-field"><label class="k-label k-form-label">${encode(end.label)}</label>` + "<div class=\"k-form-field-wrap\">" + "<ul class=\"k-radio-list\">" + "<li class=\"k-radio-item\">" + `<label class="k-radio-label"><input class="k-recur-end-never k-radio" type="radio" name="end" value="never" title="${encode(end.label)}" />${encode(end.never)}</label>` + "</li>" + "<li class=\"k-radio-item\">" + `<label class="k-radio-label"><input class="k-recur-end-count k-radio" type="radio" name="end" value="count" title="${encode(end.label)}" />${encode(end.after)}</label>` + `<input ref-recur-count aria-label="${encode(end.occurrence)}" title="${encode(end.occurrence)}" />${encode(end.occurrence)}` + "</li>" + "<li class=\"k-radio-item\">" + `<label class="k-radio-label"><input class="k-recur-end-until k-radio" type="radio" name="end" value="until" title="${encode(end.label)}" />${encode(end.on)}</label>` + `<input title="${encode(end.on)}" aria-label="${encode(end.on)}" ref-recur-until />` + "</li>" + "</ul>" + "</div>" + "</div>" : "")); var DAY_RULE = [ { day: 0, offset: 0 }, { day: 1, offset: 0 }, { day: 2, offset: 0 }, { day: 3, offset: 0 }, { day: 4, offset: 0 }, { day: 5, offset: 0 }, { day: 6, offset: 0 } ]; var WEEKDAY_RULE = [ { day: 1, offset: 0 }, { day: 2, offset: 0 }, { day: 3, offset: 0 }, { day: 4, offset: 0 }, { day: 5, offset: 0 } ]; var WEEKEND_RULE = [{ day: 0, offset: 0 }, { day: 6, offset: 0 }]; var BaseRecurrenceEditor = Widget.extend({ init: function(element, options) { var start; var that = this; var frequencies = options && options.frequencies; Widget.fn.init.call(that, element, options); that.wrapper = that.element; options = that.options; options.start = start = options.start || kendoDate.today(); if (frequencies) { options.frequencies = frequencies; } if (typeof start === "string") { options.start = kendo.parseDate(start, "yyyyMMddTHHmmss"); } if (options.firstWeekDay === null) { options.firstWeekDay = kendo.culture().calendar.firstDay; } that._namespace = "." + options.name; }, options: { value: "", start: "", timezone: "", spinners: true, firstWeekDay: null, frequencies: [ "never", "daily", "weekly", "monthly", "yearly" ], mobile: false, messages: { repeat: "Repeat", recurrenceEditorTitle: "Recurrence editor", frequencies: { never: "Never", hourly: "Hourly", daily: "Daily", weekly: "Weekly", monthly: "Monthly", yearly: "Yearly" }, hourly: { repeatEvery: "Repeat every: ", interval: " hour(s)" }, daily: { repeatEvery: "Repeat every: ", interval: " day(s)" }, weekly: { interval: " week(s)", repeatEvery: "Repeat every: ", repeatOn: "Repeat on: " }, monthly: { repeatEvery: "Repeat every: ", repeatOn: "Repeat on: ", interval: " month(s)", day: "Day ", date: "Date" }, yearly: { repeatEvery: "Repeat every: ", repeatOn: "Repeat on: ", interval: " year(s)", of: " of ", month: "month", day: "day", date: "Date" }, end: { label: "End:", mobileLabel: "Ends", never: "Never", after: "After ", occurrence: " occurrence(s)", on: "On " }, offsetPositions: { first: "first", second: "second", third: "third", fourth: "fourth", last: "last" }, weekdays: { day: "day", weekday: "weekday", weekend: "weekend day" } } }, events: ["change"], _initInterval: function() { var that = this, input = that._container.find("[ref-recur-interval]"), rule = that._value; that._interval = input.kendoNumericTextBox({ spinners: that.options.spinners, value: rule.interval || 1, decimals: 0, format: "#", min: 1, change: function() { rule.interval = this.value(); that._trigger(); } }).data("kendoNumericTextBox"); that._interval.wrapper.addClass("k-recur-interval"); }, _weekDayButtons: function(firstDay) { var that = this, names = kendo.culture().calendar.days.names, abbreviated = kendo.culture().calendar.days.namesAbbr, repeatOn = that.options.messages.weekly.repeatOn, length = names.length, idx = 0, values = [], items = []; for (; idx < length; idx++) { values.push(idx); } names = names.slice(firstDay).concat(names.slice(0, firstDay)); abbreviated = abbreviated.slice(firstDay).concat(abbreviated.slice(0, firstDay)); values = values.slice(firstDay).concat(values.slice(0, firstDay)); for (idx = 0; idx < length; idx++) { items.push({ text: abbreviated[idx], attributes: { "data-value": values[idx], "aria-label": repeatOn + " " + names[idx] } }); } if (that._container.find(".k-recur-weekday-buttons").length > 0) { that._weekDayButtonGroup = new ButtonGroup(that._container.find(".k-recur-weekday-buttons"), { items, selection: "multiple", select: function() { var rule = that._value, selectedDays = that._weekDayButtonGroup.current(); rule.weekDays = selectedDays.map(function(i, day) { return { day: Number(day.getAttribute("data-value")), offset: 0 }; }); if (!that.options.mobile) { that._trigger(); } } }); } }, _weekDayRule: function(clear) { var that = this; var weekday = (that._weekDay.element || that._weekDay).val(); var offset = Number((that._weekDayOffset.element || that._weekDayOffset).val()); var weekDays = null; var positions = null; if (!clear) { if (weekday === "day") { weekDays = DAY_RULE; positions = offset; } else if (weekday === "weekday") { weekDays = WEEKDAY_RULE; positions = offset; } else if (weekday === "weekend") { weekDays = WEEKEND_RULE; positions = offset; } else { weekDays = [{ offset, day: Number(weekday) }]; } } that._value.weekDays = weekDays; that._value.positions = positions; }, _weekDayView: function() { var that = this; var weekDays = that._value.weekDays; var positions = that._value.positions; var weekDayOffsetWidget = that._weekDayOffset; var weekDayOffset; var weekDayValue; var length; var method; if (weekDays) { length = weekDays.length; if (positions) { if (length === 7) { weekDayValue = "day"; weekDayOffset = positions; } else if (length === 5) { weekDayValue = "weekday"; weekDayOffset = positions; } else if (length === 2) { weekDayValue = "weekend"; weekDayOffset = positions; } } if (!weekDayValue) { weekDays = weekDays[0]; weekDayValue = weekDays.day; weekDayOffset = weekDays.offset || ""; } method = weekDayOffsetWidget.value ? "value" : "val"; weekDayOffsetWidget[method](weekDayOffset); that._weekDay[method](weekDayValue); } }, _initWeekDay: function() { var that = this, data; var weekdayMessage = that.options.messages.weekdays; var offsetMessage = that.options.messages.offsetPositions; var weekDayInput = that._container.find(".k-recur-weekday"); var change = function() { that._weekDayRule(); that._trigger(); }; if (weekDayInput[0]) { that._weekDayOffset = new DropDownList(that._container.find(".k-recur-weekday-offset"), { autoWidth: true, change, dataTextField: "text", dataValueField: "value", dataSource: [ { text: offsetMessage.first, value: "1" }, { text: offsetMessage.second, value: "2" }, { text: offsetMessage.third, value: "3" }, { text: offsetMessage.fourth, value: "4" }, { text: offsetMessage.last, value: "-1" } ] }); data = [ { text: weekdayMessage.day, value: "day" }, { text: weekdayMessage.weekday, value: "weekday" }, { text: weekdayMessage.weekend, value: "weekend" } ]; that._weekDay = new DropDownList(weekDayInput, { autoWidth: true, value: that.options.start.getDay(), change, dataTextField: "text", dataValueField: "value", dataSource: data.concat($.map(kendo.culture().calendar.days.names, function(dayName, idx) { return { text: dayName, value: idx }; })) }); that._weekDayView(); } }, _initWeekDays: function() { var that = this; var rule = that._value; var weekDayButtonGroup = that._weekDayButtonGroup; if (weekDayButtonGroup && rule.weekDays && rule.weekDays.length) { var ruleDays = rule.weekDays, ruleDaysLength = ruleDays.length, selectors = [], i = 0, selectedWeekDayButtons; for (; i < ruleDaysLength; i++) { selectors.push("[data-value='" + ruleDays[i].day + "']"); } selectedWeekDayButtons = weekDayButtonGroup.element.find(selectors.join(",")); weekDayButtonGroup.select(selectedWeekDayButtons); } }, _initMonthDay: function() { var that = this; var rule = that._value; var monthDayInput = that._container.find("[ref-recur-monthday]"); if (monthDayInput[0]) { that._monthDay = new kendo.ui.NumericTextBox(monthDayInput, { spinners: that.options.spinners, min: 1, max: 31, decimals: 0, format: "#", value: rule.monthDays ? rule.monthDays[0] : that.options.start.getDate(), change: function() { var value = this.value(); rule.monthDays = value ? [value] : value; that._trigger(); } }); that._monthDay.wrapper.addClass("k-recur-monthday"); } }, _initCount: function() { var that = this, input = that._container.find("[ref-recur-count]"), rule = that._value; that._count = input.kendoNumericTextBox({ spinners: that.options.spinners, value: rule.count || 1, decimals: 0, format: "#", min: 1, change: function() { rule.count = this.value(); that._trigger(); } }).data("kendoNumericTextBox"); that.