diff --git a/src/pat/recurrence/recurrence--implementation.js b/src/pat/recurrence/recurrence--implementation.js new file mode 100644 index 000000000..06e63d209 --- /dev/null +++ b/src/pat/recurrence/recurrence--implementation.js @@ -0,0 +1,1463 @@ +import $ from "jquery"; +import _ from "underscore"; +import DisplayTemplate from "./templates/display.xml"; +import FormTemplate from "./templates/form.xml"; +import OccurrenceTemplate from "./templates/occurrence.xml"; +import Modal from "../modal/modal"; +import utils from "../../core/utils"; + +// Formatting function (mostly) from jQueryTools dateinput +var Re = /d{1,4}|m{1,4}|yy(?:yy)?|"[^"]*"|'[^']*'/g; + +function zeropad(val, len) { + val = val.toString(); + len = len || 2; + while (val.length < len) { + val = "0" + val; + } + return val; +} + +function format(date, fmt, conf) { + var d = date.getDate(), + D = date.getDay(), + m = date.getMonth(), + y = date.getFullYear(), + flags = { + d: d, + dd: zeropad(d), + ddd: conf.localization.shortWeekdays[D], + dddd: conf.localization.weekdays[D], + m: m + 1, + mm: zeropad(m + 1), + mmm: conf.localization.shortMonths[m], + mmmm: conf.localization.months[m], + yy: String(y).slice(2), + yyyy: y, + }; + + var result = fmt.replace(Re, function ($0) { + return Object.prototype.hasOwnProperty.call(flags, $0) + ? flags[$0] + : $0.slice(1, $0.length - 1); + }); + + return result; +} + +function widgetSaveToRfc5545(form, RDATE, EXDATE, conf, tz) { + var value = form.find("select[name=rirtemplate]").val(); + var rtemplate = conf.rtemplate[value]; + var result = "RRULE:" + rtemplate.rrule; + var human = conf.localization.rtemplate[value]; + var interval, month, year, occurrences, day; + var weekdays, i18nweekdays, input, monthlyType, index; + var yearlyType, rangeType; + + for (const field_id of rtemplate.fields) { + const field = form.find(`#${field_id}`); + + switch (field.attr("id")) { + case "ridailyinterval": + interval = field.find("input[name=ridailyinterval]").val(); + if (interval !== "1") { + result += `;INTERVAL=${interval}`; + } + human = `${interval} ${conf.localization.dailyInterval2}`; + break; + + case "riweeklyinterval": + interval = field.find("input[name=riweeklyinterval]").val(); + if (interval !== "1") { + result += `;INTERVAL=${interval}`; + } + human = `${interval} ${conf.localization.weeklyInterval2}`; + break; + + case "riweeklyweekdays": + weekdays = []; + i18nweekdays = []; + for (let j = 0; j < conf.weekdays.length; j++) { + input = field.find(`input[name=riweeklyweekdays${j}]`); + if (input.is(":checked")) { + weekdays.push(conf.weekdays[j]); + i18nweekdays.push(conf.localization.weekdays[j]); + } + } + if (weekdays.length > 0) { + result += `;BYDAY=${weekdays.join(",")}`; + human += ` ${ + conf.localization.weeklyWeekdaysHuman + } ${i18nweekdays.join(",")}`; + } + break; + + case "rimonthlyinterval": + interval = field.find("input[name=rimonthlyinterval]").val(); + if (interval !== "1") { + result += `;INTERVAL=${interval}`; + } + human = `${interval} ${conf.localization.monthlyInterval2}`; + break; + + case "rimonthlyoptions": + monthlyType = $("input[name=rimonthlytype]:checked", form).val(); + if (monthlyType === "DAYOFMONTH") { + day = $("select[name=rimonthlydayofmonthday]", form).val(); + result += `;BYMONTHDAY=${day}`; + human += `, ${conf.localization.monthlyDayOfMonth1Human} ${day} ${conf.localization.monthlyDayOfMonth2}`; + } else if (monthlyType === "WEEKDAYOFMONTH") { + index = $("select[name=rimonthlyweekdayofmonthindex]", form).val(); + day = + conf.weekdays[ + $("select[name=rimonthlyweekdayofmonth]", form).val() + ]; + if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) { + result += `;BYDAY=${index}${day}`; + human += `, ${conf.localization.monthlyWeekdayOfMonth1Human} `; + human += `${ + conf.localization.orderIndexes[ + conf.orderIndexes.indexOf(index) + ] + } ${conf.localization.monthlyWeekdayOfMonth2} `; + human += `${ + conf.localization.weekdays[conf.weekdays.indexOf(day)] + } ${conf.localization.monthlyDayOfMonth2}`; + } + } + break; + + case "riyearlyinterval": + interval = field.find("input[name=riyearlyinterval]").val(); + if (interval !== "1") { + result += `;INTERVAL=${interval}`; + } + human = `${interval} ${conf.localization.yearlyInterval2}`; + break; + + case "riyearlyoptions": + yearlyType = $("input[name=riyearlyType]:checked", form).val(); + + if (yearlyType === "DAYOFMONTH") { + month = $("select[name=riyearlydayofmonthmonth]", form).val(); + day = $("select[name=riyearlydayofmonthday]", form).val(); + result += `;BYMONTH=${month};BYMONTHDAY=${day}`; + human += `, ${conf.localization.yearlyDayOfMonth1Human} ${ + conf.localization.months[month - 1] + } ${day}`; + } else if (yearlyType === "WEEKDAYOFMONTH") { + index = $("select[name=riyearlyweekdayofmonthindex]", form).val(); + day = + conf.weekdays[ + $("select[name=riyearlyweekdayofmonthday]", form).val() + ]; + month = $("select[name=riyearlyweekdayofmonthmonth]", form).val(); + result += `;BYMONTH=${month}`; + if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) { + result += `;BYDAY=${index}${day}`; + human += ", " + conf.localization.yearlyWeekdayOfMonth1Human; + human += + " " + + conf.localization.orderIndexes[ + $.inArray(index, conf.orderIndexes) + ]; + human += " " + conf.localization.yearlyWeekdayOfMonth2; + human += + " " + + conf.localization.weekdays[$.inArray(day, conf.weekdays)]; + human += " " + conf.localization.yearlyWeekdayOfMonth3; + human += " " + conf.localization.months[month - 1]; + human += " " + conf.localization.yearlyWeekdayOfMonth4; + } + } + break; + + case "rirangeoptions": + rangeType = form.find("input[name=rirangetype]:checked").val(); + if (rangeType === "BYOCCURRENCES") { + occurrences = form + .find("input[name=rirangebyoccurrencesvalue]") + .val(); + result += `;COUNT=${occurrences}`; + human += `, ${conf.localization.rangeByOccurrences1Human} ${occurrences} ${conf.localization.rangeByOccurrences2}`; + } else if (rangeType === "BYENDDATE") { + let date = form.find("input[name=rirangebyenddatecalendar]").val(); + if (date === "") { + const today = new Date(); + date = `${today.getFullYear()}-${(today.getMonth() + 1) + .toString() + .padStart(2, "0")}-${today.getDate()}`; + } + result += `;UNTIL=${date.replaceAll("-", "")}T000000${ + tz === true ? "Z" : "" + }`; + human += `, ${conf.localization.rangeByEndDateHuman} `; + var date_parts = date.split("-"); + human += format( + new Date(date_parts[0], date_parts[1] - 1, date_parts[2]), + conf.localization.longDateFormat, + conf, + ); + } + break; + } + } + + if (RDATE.length) { + RDATE.sort(); + let tmp_dates = []; + let tmp_human = []; + + // make sure our additional RDATE dates have the same start time + // XXX: not used, remove if superfluous + //const rdate_time = start_date + // ? `T${start_date.getHours()}:${start_date.getMinutes()}:00` + // : "T00:00:00"; + + for (let rdate of RDATE) { + if (rdate !== "") { + // The values from the input field are ISO8601 "YYYY-MM-DD" + // RFC5545 expects a date or date-time format of e.g. + // "YYYYMMDD". See:‌ + // https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2 + rdate = rdate.replaceAll("-", ""); + // by adding "T000000" the recurrence sequence generator of + // plone.event.recurrence adds the current start time correctly + rdate += rdate.length === 8 ? "T000000" : ""; + rdate += tz ? "Z" : ""; + tmp_dates.push(rdate); + + // human readable RDATE + year = parseInt(rdate.substring(0, 4), 10); + month = parseInt(rdate.substring(4, 6), 10) - 1; // js month. + day = parseInt(rdate.substring(6, 8), 10); + tmp_human.push( + format( + new Date(year, month, day), + conf.localization.longDateFormat, + conf, + ), + ); + } + } + if (tmp_dates.length) { + result += `\nRDATE:${tmp_dates.join(",")}`; + human += `${conf.localization.including} ${tmp_human.join("; ")}`; + } + } + + if (EXDATE.length) { + EXDATE.sort(); + let tmp_dates = []; + let tmp_human = []; + for (let exdate of EXDATE) { + if (exdate !== "") { + // EXDATE values are "YYYYMMDDTHHMMZ" + tmp_dates.push(exdate); + + // human readable EXDATE + year = parseInt(exdate.substring(0, 4), 10); + month = parseInt(exdate.substring(4, 6), 10) - 1; + day = parseInt(exdate.substring(6, 8), 10); + tmp_human.push( + format( + new Date(year, month, day), + conf.localization.longDateFormat, + conf, + ), + ); + } + } + if (tmp_dates.length) { + result += `\nEXDATE:${tmp_dates.join(",")}`; + human += `${conf.localization.except} ${tmp_human.join("; ")}`; + } + } + + return { result: result, description: human }; +} + +function parseLine(icalline) { + var result = {}; + var pos = icalline.indexOf(":"); + var property = icalline.substring(0, pos); + result.value = icalline.substring(pos + 1); + + if (property.indexOf(";") !== -1) { + pos = property.indexOf(";"); + result.parameters = property.substring(pos + 1); + result.property = property.substring(0, pos); + } else { + result.parameters = null; + result.property = property; + } + return result; +} + +function cleanDates(dates) { + // Get rid of timezones + // TODO: We could parse dates and range here, maybe? + var result = []; + + for (const date of dates.split(",")) { + if (date.indexOf("Z") !== -1) { + result.push(date.substring(0, 15)); + } else { + result.push(date); + } + } + return result; +} + +function parseIcal(icaldata) { + var result = { + RRULE: "", + RDATE: [], + EXDATE: [], + }; + var line = null; + var nextline; + + var lines = icaldata.split("\n"); + lines.reverse(); + while (line !== "") { + if (lines.length > 0) { + nextline = lines.pop(); + if (nextline.charAt(0) === " " || nextline.charAt(0) === "\t") { + // Line continuation: + line = line + nextline; + continue; + } + } else { + nextline = ""; + } + + // New line; the current one is finished, add it to the result. + if (line !== null) { + line = parseLine(line); + // We ignore properties for now + if (line.property === "RDATE" || line.property === "EXDATE") { + result[line.property] = cleanDates(line.value); + } else { + result[line.property] = line.value; + } + } + + line = nextline; + } + return result; +} + +function widgetLoadFromRfc5545(form, conf, icaldata, force) { + var unsupportedFeatures = []; + var matches, rtemplate, d, input, index; + var selectors, field, radiobutton; + var freq, interval, byday, bymonth, bymonthday, count, until; + var day, month, year, weekday; + + if (icaldata.RRULE === undefined) { + unsupportedFeatures.push(conf.localization.noRule); + if (!force) { + return -1; // Fail! + } + } else { + matches = /FREQ=([^;]+);?/.exec(icaldata.RRULE); + if (matches) { + freq = matches[1]; + } else { + freq = "DAILY"; + } + + matches = /INTERVAL=([0-9]+);?/.exec(icaldata.RRULE); + if (matches) { + interval = matches[1]; + } else { + interval = "1"; + } + + matches = /BYDAY=([^;]+);?/.exec(icaldata.RRULE); + if (matches) { + byday = matches[1]; + } else { + byday = ""; + } + + matches = /BYMONTHDAY=([^;]+);?/.exec(icaldata.RRULE); + if (matches) { + bymonthday = matches[1].split(","); + } else { + bymonthday = null; + } + + matches = /BYMONTH=([^;]+);?/.exec(icaldata.RRULE); + if (matches) { + bymonth = matches[1].split(","); + } else { + bymonth = null; + } + + matches = /COUNT=([0-9]+);?/.exec(icaldata.RRULE); + if (matches) { + count = matches[1]; + } else { + count = null; + } + + matches = /UNTIL=([0-9T]+);?/.exec(icaldata.RRULE); + if (matches) { + until = matches[1]; + } else { + until = null; + } + + matches = /BYSETPOS=([^;]+);?/.exec(icaldata.RRULE); + if (matches) { + unsupportedFeatures.push(conf.localization.bysetpos); + } + + // Find the best rule: + if (Object.prototype.hasOwnProperty.call(conf.rtemplate, freq.toLowerCase())) { + rtemplate = conf.rtemplate[freq.toLowerCase()]; + } else { + for (freq of conf.rtemplate) { + /* fallback to first available template if no match above*/ + rtemplate = conf.rtemplate[freq]; + break; + } + unsupportedFeatures.push(conf.localization.noTemplateMatch); + } + + // set rirtemplate selector to computed value + form.find("select[name='rirtemplate']").val(freq.toLowerCase()); + + for (const field_id of rtemplate.fields) { + field = form.find(`#${field_id}`); + switch (field.attr("id")) { + case "ridailyinterval": + field.find("input[name=ridailyinterval]").val(interval); + break; + + case "riweeklyinterval": + field.find("input[name=riweeklyinterval]").val(interval); + break; + + case "riweeklyweekdays": + if (byday.length === 0) break; + byday = byday.split(","); + for (d = 0; d < conf.weekdays.length; d++) { + input = field.find(`input[name="riweeklyweekdays${d}"]`); + if (input.length === 0) continue; + day = conf.weekdays[d]; + input.attr("checked", byday.includes(day)); + } + break; + + case "rimonthlyinterval": + field.find("input[name=rimonthlyinterval]").val(interval); + break; + + case "rimonthlyoptions": + var monthlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY + + if (bymonthday) { + monthlyType = "DAYOFMONTH"; + if (bymonthday.length > 1) { + // No support for multiple days in one month + unsupportedFeatures.push( + conf.localization.multipleDayOfMonth, + ); + // Just keep the first + bymonthday = bymonthday[0]; + } + field + .find("select[name=rimonthlydayofmonthday]") + .val(bymonthday); + } + + if (byday) { + monthlyType = "WEEKDAYOFMONTH"; + + if (byday.indexOf(",") !== -1) { + // No support for multiple days in one month + unsupportedFeatures.push( + conf.localization.multipleDayOfMonth, + ); + byday = byday.split(",")[0]; + } + index = byday.slice(0, -2); + if (index.charAt(0) !== "+" && index.charAt(0) !== "-") { + index = "+" + index; + } + weekday = byday.slice(-2); + field + .find("select[name=rimonthlyweekdayofmonthindex]") + .val(index); + const day_index = conf.weekdays.indexOf(weekday); + field + .find("select[name=rimonthlyweekdayofmonth]") + .val(day_index); + } + + selectors = field.find("input[name=rimonthlytype]"); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr( + "checked", + radiobutton.value === monthlyType, + ); + } + break; + + case "riyearlyinterval": + field.find("input[name=riyearlyinterval]").val(interval); + break; + + case "riyearlyoptions": + var yearlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY + + if (bymonthday) { + yearlyType = "DAYOFMONTH"; + if (bymonthday.length > 1) { + // No support for multiple days in one month + unsupportedFeatures.push( + conf.localization.multipleDayOfMonth, + ); + bymonthday = bymonthday[0]; + } + field.find("select[name=riyearlydayofmonthmonth]").val(bymonth); + field.find("select[name=riyearlydayofmonthday]").val(bymonthday); + } + + if (byday) { + yearlyType = "WEEKDAYOFMONTH"; + + if (byday.indexOf(",") !== -1) { + // No support for multiple days in one month + unsupportedFeatures.push( + conf.localization.multipleDayOfMonth, + ); + byday = byday.split(",")[0]; + } + index = byday.slice(0, -2); + if (index.charAt(0) !== "+" && index.charAt(0) !== "-") { + index = "+" + index; + } + weekday = byday.slice(-2); + field + .find("select[name=riyearlyweekdayofmonthindex]") + .val(index); + + const weekday_index = conf.weekdays.indexOf(weekday); + field + .find("select[name=riyearlyweekdayofmonthday]") + .val(weekday_index); + field + .find("select[name=riyearlyweekdayofmonthmonth]") + .val(bymonth); + } + + selectors = field.find("input[name=riyearlyType]"); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr("checked", radiobutton.value === yearlyType); + } + break; + + case "rirangeoptions": + // default value per configuration + var rangeType = conf.hasRepeatForeverButton + ? "NOENDDATE" + : "BYOCCURRENCES"; + + if (count) { + rangeType = "BYOCCURRENCES"; + field.find("input[name=rirangebyoccurrencesvalue]").val(count); + } + + if (until) { + rangeType = "BYENDDATE"; + input = field.find("input[name=rirangebyenddatecalendar]"); + year = until.slice(0, 4); + month = until.slice(4, 6); + day = until.slice(6, 8); + input.val(`${year}-${month}-${day}`); + } + + selectors = field.find("input[name=rirangetype]"); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr("checked", radiobutton.value === rangeType); + } + break; + } + } + } + + var messagearea = form.find("#messagearea"); + if (unsupportedFeatures.length !== 0) { + messagearea.text( + conf.localization.unsupportedFeatures + " " + unsupportedFeatures.join("; "), + ); + messagearea.show(); + return 1; + } else { + messagearea.text(""); + messagearea.hide(); + return 0; + } +} + +const RecurrenceInput = function (conf, textarea) { + var self = this; + var $textarea = $(textarea); + + // initalize parsed icaldata + if (textarea.innerHTML) { + textarea["ical"] = parseIcal(textarea.innerHTML); + } else { + textarea["ical"] = { + RRULE: "", + RDATE: [], + EXDATE: [], + }; + } + + // Extend conf with non-configurable data used by templates. + var orderedWeekdays = []; + var now_date = new Date().toISOString().substring(0, 10); + var index; + + for (let i = 0; i < 7; i++) { + index = i + conf.firstDay; + if (index > 6) { + index = index - 7; + } + orderedWeekdays.push(index); + } + + conf = { + ...conf, + orderIndexes: ["+1", "+2", "+3", "+4", "-1"], + weekdays: ["SU", "MO", "TU", "WE", "TH", "FR", "SA"], + orderedWeekdays: orderedWeekdays, + }; + + // The recurrence type dropdown should show certain fields depending + // on selection: + function displayFields(selector) { + // First hide all the fields + self.$modalForm.find(".rifield").hide(); + // Then show the ones that should be shown. + var value = selector.val(); + if (value) { + for (const rtField of conf.rtemplate[value].fields) { + self.$modalForm.find(`#${rtField}`).show(); + } + } + } + + function occurrenceExclude(event) { + event.preventDefault(); + textarea["ical"].EXDATE.push(this.attributes.date.value); + var $this = $(this); + $this.addClass("exdate"); + $this.parent().parent().addClass("exdate"); + $this.off("click").on("click", occurrenceInclude); + } + + function occurrenceInclude(event) { + event.preventDefault(); + textarea["ical"].EXDATE.splice( + $.inArray(this.attributes.date.value, textarea["ical"].EXDATE), + 1, + ); + var $this = $(this); + $this.removeClass("exdate"); + $this.parent().parent().removeClass("exdate"); + $this.off("click").on("click", occurrenceExclude); + } + + function occurrenceDelete(event) { + event.preventDefault(); + textarea["ical"].RDATE.splice( + $.inArray(this.attributes.date.value, textarea["ical"].RDATE), + 1, + ); + $(this) + .parent() + .parent() + .hide("slow", function () { + $(this).remove(); + }); + } + + function occurrenceAdd(event) { + event.preventDefault(); + var datevalue = self.$modalForm.find(".riaddoccurrence input#adddate").val(); + var date_parts = datevalue.split("-"); + datevalue += datevalue.length === 10 ? "T000000" : ""; + var errorarea = self.$modalForm.find(".riaddoccurrence div.alert"); + errorarea.text(""); + errorarea.hide(); + + // Add date only if it is not already in RDATE + if (!textarea["ical"].RDATE.includes(datevalue)) { + textarea["ical"].RDATE.push(datevalue); + var $newdate = + $(`
+ + ${format( + new Date(date_parts[0], date_parts[1] - 1, date_parts[2]), + conf.localization.longDateFormat, + conf, + )}, + ${conf.localization.additionalDate} + + + + +
`); + $newdate.hide(); + self.$modalForm.find("div.rioccurrences").prepend($newdate); + $newdate.slideDown(); + $newdate.find("button.rdate").on("click", occurrenceDelete); + } else { + errorarea.text(conf.localization.alreadyAdded).show(); + } + } + + // element is where to find the tag in question. Can be the form + // or the display widget. Defaults to the self.$modalForm. + function loadOccurrences(startdate, rfc5545, start, readonly) { + var element, occurrenceDiv; + + if (!readonly) { + element = self.$modalForm; + } else { + element = self.display; + } + + occurrenceDiv = element.find(".rioccurrences"); + + const year = startdate.getFullYear(); + const month = startdate.getMonth() + 1; + const day = startdate.getDate(); + + var data = { + year: year, + month: month, // Sending January as 0? I think not. + day: day, + rrule: rfc5545, + format: conf.localization.longDateFormat, + start: start, + }; + + $.ajax({ + url: conf.ajaxURL, + async: false, // Can't be tested if it's asynchronous, annoyingly. + type: "post", + dataType: "json", + contentType: conf.ajaxContentType, + cache: false, + data: data, + success: function (resp) { + var result; + + resp.readOnly = readonly; + resp.localization = conf.localization; + resp.icons = conf.icons; + + // Format dates: + var date, y, m, d; + for (let occurrence of resp.occurrences) { + date = occurrence.date; + y = parseInt(date.substring(0, 4), 10); + m = parseInt(date.substring(4, 6), 10) - 1; // jan=0 + d = parseInt(date.substring(6, 8), 10); + occurrence.date = `${y}-${zeropad(m + 1)}-${zeropad(d)}T000000`; + occurrence.formattedDate = format( + new Date(y, m, d), + conf.localization.longDateFormat, + conf, + ); + } + + result = _.template(OccurrenceTemplate)(resp); + occurrenceDiv.replaceWith(result); + + // Add the batch actions: + element.find(".rioccurrences .batching a").on("click", function (event) { + event.stopPropagation(); + event.preventDefault(); + loadOccurrences( + startdate, + rfc5545, + this.attributes.start.value, + readonly, + ); + }); + + // Add the delete/undelete actions: + if (!readonly) { + element + .find(".rioccurrences .action button.rrule") + .on("click", occurrenceExclude); + element + .find(".rioccurrences .action button.exdate") + .on("click", occurrenceInclude); + element + .find(".rioccurrences .action button.rdate") + .on("click", occurrenceDelete); + } + }, + error: function () { + occurrenceDiv[0].innerHTML = ` +
${conf.localization.error_load_occurrences}
+ `; + }, + }); + } + + function getField(field) { + // See if it is a field already + var realField = $(field); + if (!realField.length) { + // Otherwise, we assume it's an id: + realField = $("#" + field); + } + if (!realField.length) { + // Still not? Then it's a name. + realField = $("input[name='" + field + "']"); + } + return realField; + } + function findStartDate() { + var startdate = null; + var startField, startFieldYear, startFieldMonth, startFieldDay; + + // Find the default byday and bymonthday from the start date, if any: + if (conf.startField) { + startField = getField(conf.startField); + if (!startField.length) { + // Field not found + return null; + } + startdate = startField.val(); + if (startdate === "") { + // Probably not an input at all. Try to see if it contains a date + startdate = startField.text(); + } + + if (typeof startdate === "string") { + // convert human readable, non ISO8601 dates, like + // '2014-04-24 19:00', where the 'T' separator is missing. + startdate = startdate.replace(" ", "T"); + } + + startdate = new Date(startdate); + } else if (conf.startFieldYear && conf.startFieldMonth && conf.startFieldDay) { + startFieldYear = getField(conf.startFieldYear); + startFieldMonth = getField(conf.startFieldMonth); + startFieldDay = getField(conf.startFieldDay); + if ( + !startFieldYear.length && + !startFieldMonth.length && + !startFieldDay.length + ) { + // Field not found + return null; + } + startdate = new Date( + startFieldYear.val(), + startFieldMonth.val() - 1, + startFieldDay.val(), + ); + } + if (startdate === null) { + return null; + } + // We have some sort of startdate: + if (isNaN(startdate)) { + return null; + } + return startdate; + } + function findEndDate() { + var endField, enddate; + + endField = self.$modalForm.find("input[name=rirangebyenddatecalendar]"); + enddate = endField.val(); + enddate = new Date(enddate); + + // if the end date is incorrect or the field is left empty + if (isNaN(enddate) || endField.val() === "") { + return null; + } + return enddate; + } + function findIntField(fieldName) { + var field, num; + + field = self.$modalForm.find("input[name=" + fieldName + "]"); + + num = field.val(); + + // if it's not a number or the field is left empty + if (isNaN(num) || num.toString().indexOf(".") !== -1 || field.val() === "") { + return null; + } + return num; + } + + // Loading (populating) display and form widget with + // passed RFC5545 string (data) + function loadData(form) { + widgetLoadFromRfc5545(form, conf, textarea["ical"], true); + + const startdate = findStartDate(); + + if (startdate !== null) { + // If the date is a real date, set the defaults in the form + document.querySelector("select[name=rimonthlydayofmonthday]").value = + startdate.getDate(); + const dayindex = + conf.orderIndexes[Math.floor((startdate.getDate() - 1) / 7)]; + const day = conf.weekdays[startdate.getDay()]; + document.querySelector("select[name=rimonthlyweekdayofmonthindex]").value = + dayindex; + document.querySelector("select[name=rimonthlyweekdayofmonth]").value = day; + + document.querySelector("select[name=riyearlydayofmonthmonth]").value = + startdate.getMonth() + 1; + document.querySelector("select[name=riyearlydayofmonthday]").value = + startdate.getDate(); + document.querySelector("select[name=riyearlyweekdayofmonthindex]").value = + dayindex; + document.querySelector("select[name=riyearlyweekdayofmonthday]").value = day; + document.querySelector("select[name=riyearlyweekdayofmonthmonth]").value = + startdate.getMonth() + 1; + + // Now when we have a start date, we can also do an ajax call to calculate occurrences: + var rfc5545 = + textarea.innerHTML || + widgetSaveToRfc5545( + form, + textarea["ical"].RDATE, + textarea["ical"].EXDATE, + conf, + false, + ).result; + + loadOccurrences(startdate, rfc5545, 0, false); + + // Show the add and refresh buttons: + document.querySelector("div.rioccurrencesactions").style.display = "block"; + } else { + // No EXDATE/RDATE support + document.querySelector("div.rioccurrencesactions").style.display = "none"; + } + + displayFields(form.find("select[name=rirtemplate]")); + } + + function recurrenceOn(form) { + var RFC5545 = widgetSaveToRfc5545( + form, + textarea["ical"].RDATE, + textarea["ical"].EXDATE, + conf, + false, + ); + var label = self.display.find("label[class=ridisplay-label]"); + label.text(conf.localization.displayActivate + " " + RFC5545.description); + textarea["ical"] = parseIcal(RFC5545.result); + textarea.innerHTML = RFC5545.result; + $textarea.trigger("change"); + var startdate = findStartDate(); + if (startdate !== null) { + loadOccurrences(startdate, RFC5545.result, 0, true); + } + self.display.find('button[name="riedit"]').text(conf.localization.edit_rules); + self.display.find('button[name="ridelete"]').show(); + } + + function recurrenceOff() { + var label = self.display.find("label[class=ridisplay-label]"); + label.text(conf.localization.displayUnactivate); + // reset ical object + textarea["ical"] = { + RRULE: "", + RDATE: [], + EXDATE: [], + }; + textarea.innerHTML = ""; + $textarea.trigger("change"); // Clear the textarea. + self.display.find(".rioccurrences").hide(); + self.display.find('button[name="riedit"]').text(conf.localization.add_rules); + self.display.find('button[name="ridelete"]').hide(); + } + + function checkFields(form) { + var startDate, endDate, num, messagearea; + startDate = findStartDate(); + + // Hide any error message from before + messagearea = self.$modalForm.find("#messagearea"); + messagearea.text(""); + messagearea.hide(); + + // Hide add field errors + self.$modalForm.find(".riaddoccurrence div.alert").text("").hide(); + + // Repeats Daily + if (self.$modalForm.find("#ridailyinterval").css("display") === "block") { + // Check repeat every field + num = findIntField("ridailyinterval", form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.localization.noRepeatEvery).show(); + return false; + } + } + + // Repeats Weekly + if (self.$modalForm.find("#riweeklyinterval").css("display") === "block") { + // Check repeat every field + num = findIntField("riweeklyinterval", form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.localization.noRepeatEvery).show(); + return false; + } + } + + // Repeats Monthly + if (self.$modalForm.find("#rimonthlyinterval").css("display") === "block") { + // Check repeat every field + num = findIntField("rimonthlyinterval", form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.localization.noRepeatEvery).show(); + return false; + } + + // Check repeat on + if (self.$modalForm.find("#rimonthlyoptions input:checked").length === 0) { + messagearea.text(conf.localization.noRepeatOn).show(); + return false; + } + } + + // Repeats Yearly + if (self.$modalForm.find("#riyearlyinterval").css("display") === "block") { + // Check repeat every field + num = findIntField("riyearlyinterval", form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.localization.noRepeatEvery).show(); + return false; + } + + // Check repeat on + if (self.$modalForm.find("#riyearlyoptions input:checked").length === 0) { + messagearea.text(conf.localization.noRepeatOn).show(); + return false; + } + } + + // End recurrence fields + + // If after N occurences is selected, check its value + if ( + self.$modalForm.find('input[value="BYOCCURRENCES"]:visible:checked').length > + 0 + ) { + num = findIntField("rirangebyoccurrencesvalue", form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.localization.noEndAfterNOccurrences).show(); + return false; + } + } + + // If end date is selected, check its value + if ( + self.$modalForm.find('input[value="BYENDDATE"]:visible:checked').length > 0 + ) { + endDate = findEndDate(form); + if (!endDate) { + // if endDate is null that means the field is empty + messagearea.text(conf.localization.noEndDate).show(); + return false; + } else if (endDate < startDate) { + // the end date cannot be before start date + messagearea.text(conf.localization.pastEndDate).show(); + return false; + } + } + + return true; + } + + function save(event) { + event.preventDefault(); + // if no field errors, process the request + if (checkFields(self.$modalForm)) { + // close modal + self.modal.hide(); + recurrenceOn(self.$modalForm); + self.$modalForm = null; + } + } + + function cancel(event) { + event.preventDefault(); + // close modal + self.modal.hide(); + self.$modalForm = null; + } + + function updateOccurrences() { + var startDate; + startDate = findStartDate(); + + // if no field errors, process the request + if (checkFields(form)) { + loadOccurrences( + startDate, + widgetSaveToRfc5545( + self.$modalForm, + textarea["ical"].RDATE, + textarea["ical"].EXDATE, + conf, + false, + ).result, + 0, + false, + ); + } + } + + function initModalForm() { + var enddate = self.$modalForm.find("input[name=rirangebyenddatecalendar]"); + if (!enddate.val()) { + var start_date = findStartDate(); + start_date.setDate(start_date.getDate() + 1); + enddate.val(start_date.toISOString().substring(0, 10)); + } + + self.$modalForm.find("input#addaction").on("click", occurrenceAdd); + + // When selecting template, update what fieldsets are visible and the occurrences. + self.$modalForm.find('select[name="rirtemplate"]').on("change", function () { + displayFields($(this)); + updateOccurrences(); + }); + + // Update the selected dates section + self.$modalForm + .find( + ` + input:radio, + input:checkbox, + .riweeklyweekday > input, + input[name=ridailyinterval], + input[name=riweeklyinterval], + input[name=rimonthlyinterval], + input[name=riyearlyinterval], + input[name=rirangebyoccurrencesvalue], + input[name=rirangebyenddatecalendar], + #rimonthlyoptions select, + #riyearlyoptions select, + #riyearlyinterval select`, + ) + .on("change", function () { + // Update only if the occurances are shown + if (self.$modalForm.find(".rioccurrencesactions:visible").length !== 0) { + updateOccurrences(); + } + }); + + // initialize occurrence adddate value + self.$modalForm.find("div.riaddoccurrence input#adddate").val(now_date); + + /* + Save and cancel methods: + */ + self.$modalForm.find(".ricancelbutton").on("click", cancel); + self.$modalForm.find(".risavebutton").on("click", save); + } + + /* + Load the templates + */ + + self.display = $(_.template(DisplayTemplate)(conf)); + var form = $(_.template(FormTemplate)(conf)); + + form.appendTo("body"); + form.hide(); + + // Make an modal and hide it + self.modal = new Modal(form, { + content: "div.riform", + buttons: ".ributtons > button", + modalSizeClass: "modal-lg", + backdropOptions: { + opacity: "0.5", + }, + }); + + if (textarea.innerHTML) { + var result = widgetLoadFromRfc5545(form, conf, textarea["ical"], false); + if (result === -1) { + var label = self.display.find("label[class=ridisplay-label]"); + label.text(conf.localization.noRule); + } else { + recurrenceOn(form); + } + } + + /* + Do all the GUI stuff: + */ + + // When you click "Delete...", the recurrence rules should be cleared. + self.display.find('button[name="ridelete"]').on("click", function (e) { + e.preventDefault(); + recurrenceOff(); + }); + + // Show form modal when you click on the "Edit..." link + self.display.find('button[name="riedit"]').on("click", function (e) { + // Load the form to set up the right fields to show, etc. + e.preventDefault(); + self.modal.show(); + self.$modalForm = $("form", self.modal.$modalContent); + loadData(self.$modalForm); + initModalForm(); + }); +}; + +// Pattern properties: grafted onto the registered thin pattern in recurrence.js. +export default { + defaults: { + localization: { + displayUnactivate: "Does not repeat", + displayActivate: "Repeats every", + add_rules: "Add", + edit_rules: "Edit", + delete_rules: "Delete", + add: "Add", + refresh: "Refresh", + + title: "Repeat", + preview: "Selected dates", + addDate: "Add date", + + recurrenceType: "Repeats:", + + dailyInterval1: "Repeat every:", + dailyInterval2: "days", + + weeklyInterval1: "Repeat every:", + weeklyInterval2: "week(s)", + weeklyWeekdays: "Repeat on:", + weeklyWeekdaysHuman: "on:", + + monthlyInterval1: "Repeat every:", + monthlyInterval2: "month(s)", + monthlyDayOfMonth1: "Day", + monthlyDayOfMonth1Human: "on day", + monthlyDayOfMonth2: "of the month", + monthlyDayOfMonth3: "month(s)", + monthlyWeekdayOfMonth1: "The", + monthlyWeekdayOfMonth1Human: "on the", + monthlyWeekdayOfMonth2: "", + monthlyWeekdayOfMonth3: "of the month", + monthlyRepeatOn: "Repeat on:", + + yearlyInterval1: "Repeat every:", + yearlyInterval2: "year(s)", + yearlyDayOfMonth1: "Every", + yearlyDayOfMonth1Human: "on", + yearlyDayOfMonth2: "", + yearlyDayOfMonth3: "", + yearlyWeekdayOfMonth1: "The", + yearlyWeekdayOfMonth1Human: "on the", + yearlyWeekdayOfMonth2: "", + yearlyWeekdayOfMonth3: "of", + yearlyWeekdayOfMonth4: "", + yearlyRepeatOn: "Repeat on:", + + range: "End recurrence:", + rangeNoEnd: "Never", + rangeByOccurrences1: "After", + rangeByOccurrences1Human: "ends after", + rangeByOccurrences2: "occurrence(s)", + rangeByEndDate: "On", + rangeByEndDateHuman: "ends on", + + including: ", and also", + except: ", except for", + + cancel: "Cancel", + save: "Save", + + recurrenceStart: "Start of the recurrence", + additionalDate: "Additional date", + include: "Include", + exclude: "Exclude", + remove: "Remove", + + orderIndexes: ["first", "second", "third", "fourth", "last"], + months: [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + shortMonths: [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ], + weekdays: [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ], + shortWeekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + + longDateFormat: "mmmm dd, yyyy", + shortDateFormat: "mm/dd/yyyy", + + unsupportedFeatures: + "Warning: This event uses recurrence features not " + + "supported by this widget. Saving the recurrence " + + "may change the recurrence in unintended ways:", + noTemplateMatch: "No matching recurrence template", + multipleDayOfMonth: + "This widget does not support multiple days in monthly or yearly recurrence", + bysetpos: "BYSETPOS is not supported", + noRule: "No RRULE in RRULE data", + noRepeatEvery: 'Error: The "Repeat every"-field must be between 1 and 1000', + noEndDate: "Error: End date is not set. Please set a correct value", + noRepeatOn: 'Error: "Repeat on"-value must be selected', + pastEndDate: "Error: End date cannot be before start date", + noEndAfterNOccurrences: + 'Error: The "After N occurrences"-field must be between 1 and 1000', + alreadyAdded: "This date was already added", + + rtemplate: { + daily: "Daily", + mondayfriday: "Monday and Friday", + weekdays: "Weekday", + weekly: "Weekly", + monthly: "Monthly", + yearly: "Yearly", + }, + + error_load_occurrences: "Cannot load the occurrences preview.", + }, + + readOnly: false, + firstDay: 0, + + // "REMOTE" FIELD + startField: null, + startFieldYear: null, + startFieldMonth: null, + startFieldDay: null, + ajaxURL: null, + ajaxContentType: "application/json; charset=utf8", + ributtonExtraClass: "", + + // INPUT CONFIGURATION + allowAdditionalDates: false, + hasRepeatForeverButton: true, + + // JQUERY TEMPLATE NAMES + template: { + form: "#jquery-recurrenceinput-form-tmpl", + display: "#jquery-recurrenceinput-display-tmpl", + }, + + // RECURRENCE TEMPLATES + rtemplate: { + daily: { + rrule: "FREQ=DAILY", + fields: ["ridailyinterval", "rirangeoptions"], + }, + mondayfriday: { + rrule: "FREQ=WEEKLY;BYDAY=MO,FR", + fields: ["rirangeoptions"], + }, + weekdays: { + rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", + fields: ["rirangeoptions"], + }, + weekly: { + rrule: "FREQ=WEEKLY", + fields: ["riweeklyinterval", "riweeklyweekdays", "rirangeoptions"], + }, + monthly: { + rrule: "FREQ=MONTHLY", + fields: ["rimonthlyinterval", "rimonthlyoptions", "rirangeoptions"], + }, + yearly: { + rrule: "FREQ=YEARLY", + fields: ["riyearlyinterval", "riyearlyoptions", "rirangeoptions"], + }, + }, + }, + + load_icons: async function () { + return { + reload: await utils.resolveIcon("arrow-clockwise"), + remove: await utils.resolveIcon("trash"), + exclude: await utils.resolveIcon("calendar-x"), + include: await utils.resolveIcon("plus-circle"), + }; + }, + + init: async function () { + // tmpl BEFORE recurrenceinput + import("./recurrence.scss"); + + this.el.classList.add("recurrence-widget"); + var icons = await this.load_icons(); + + var config = { + ...this.options, + ...this.options.configuration, + icons: icons, + }; + + // our recurrenceinput widget instance + var recurrenceinput = new RecurrenceInput(config, this.el); + // hide textarea and place display widget after textarea + this.$el.after(recurrenceinput.display); + + // hide the textarea + this.el.style.display = "none"; + }, +}; diff --git a/src/pat/recurrence/recurrence.js b/src/pat/recurrence/recurrence.js index 04746548c..19f02174e 100644 --- a/src/pat/recurrence/recurrence.js +++ b/src/pat/recurrence/recurrence.js @@ -1,1466 +1,21 @@ import $ from "jquery"; -import _ from "underscore"; import Base from "@patternslib/patternslib/src/core/base"; -import DisplayTemplate from "./templates/display.xml"; -import FormTemplate from "./templates/form.xml"; -import OccurrenceTemplate from "./templates/occurrence.xml"; -import Modal from "../modal/modal"; -import utils from "../../core/utils"; - -// Formatting function (mostly) from jQueryTools dateinput -var Re = /d{1,4}|m{1,4}|yy(?:yy)?|"[^"]*"|'[^']*'/g; - -function zeropad(val, len) { - val = val.toString(); - len = len || 2; - while (val.length < len) { - val = "0" + val; - } - return val; -} - -function format(date, fmt, conf) { - var d = date.getDate(), - D = date.getDay(), - m = date.getMonth(), - y = date.getFullYear(), - flags = { - d: d, - dd: zeropad(d), - ddd: conf.localization.shortWeekdays[D], - dddd: conf.localization.weekdays[D], - m: m + 1, - mm: zeropad(m + 1), - mmm: conf.localization.shortMonths[m], - mmmm: conf.localization.months[m], - yy: String(y).slice(2), - yyyy: y, - }; - - var result = fmt.replace(Re, function ($0) { - return Object.prototype.hasOwnProperty.call(flags, $0) - ? flags[$0] - : $0.slice(1, $0.length - 1); - }); - - return result; -} - -function widgetSaveToRfc5545(form, RDATE, EXDATE, conf, tz) { - var value = form.find("select[name=rirtemplate]").val(); - var rtemplate = conf.rtemplate[value]; - var result = "RRULE:" + rtemplate.rrule; - var human = conf.localization.rtemplate[value]; - var interval, month, year, occurrences, day; - var weekdays, i18nweekdays, input, monthlyType, index; - var yearlyType, rangeType; - - for (const field_id of rtemplate.fields) { - const field = form.find(`#${field_id}`); - - switch (field.attr("id")) { - case "ridailyinterval": - interval = field.find("input[name=ridailyinterval]").val(); - if (interval !== "1") { - result += `;INTERVAL=${interval}`; - } - human = `${interval} ${conf.localization.dailyInterval2}`; - break; - - case "riweeklyinterval": - interval = field.find("input[name=riweeklyinterval]").val(); - if (interval !== "1") { - result += `;INTERVAL=${interval}`; - } - human = `${interval} ${conf.localization.weeklyInterval2}`; - break; - - case "riweeklyweekdays": - weekdays = []; - i18nweekdays = []; - for (let j = 0; j < conf.weekdays.length; j++) { - input = field.find(`input[name=riweeklyweekdays${j}]`); - if (input.is(":checked")) { - weekdays.push(conf.weekdays[j]); - i18nweekdays.push(conf.localization.weekdays[j]); - } - } - if (weekdays.length > 0) { - result += `;BYDAY=${weekdays.join(",")}`; - human += ` ${ - conf.localization.weeklyWeekdaysHuman - } ${i18nweekdays.join(",")}`; - } - break; - - case "rimonthlyinterval": - interval = field.find("input[name=rimonthlyinterval]").val(); - if (interval !== "1") { - result += `;INTERVAL=${interval}`; - } - human = `${interval} ${conf.localization.monthlyInterval2}`; - break; - - case "rimonthlyoptions": - monthlyType = $("input[name=rimonthlytype]:checked", form).val(); - if (monthlyType === "DAYOFMONTH") { - day = $("select[name=rimonthlydayofmonthday]", form).val(); - result += `;BYMONTHDAY=${day}`; - human += `, ${conf.localization.monthlyDayOfMonth1Human} ${day} ${conf.localization.monthlyDayOfMonth2}`; - } else if (monthlyType === "WEEKDAYOFMONTH") { - index = $("select[name=rimonthlyweekdayofmonthindex]", form).val(); - day = - conf.weekdays[ - $("select[name=rimonthlyweekdayofmonth]", form).val() - ]; - if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) { - result += `;BYDAY=${index}${day}`; - human += `, ${conf.localization.monthlyWeekdayOfMonth1Human} `; - human += `${ - conf.localization.orderIndexes[ - conf.orderIndexes.indexOf(index) - ] - } ${conf.localization.monthlyWeekdayOfMonth2} `; - human += `${ - conf.localization.weekdays[conf.weekdays.indexOf(day)] - } ${conf.localization.monthlyDayOfMonth2}`; - } - } - break; - - case "riyearlyinterval": - interval = field.find("input[name=riyearlyinterval]").val(); - if (interval !== "1") { - result += `;INTERVAL=${interval}`; - } - human = `${interval} ${conf.localization.yearlyInterval2}`; - break; - - case "riyearlyoptions": - yearlyType = $("input[name=riyearlyType]:checked", form).val(); - - if (yearlyType === "DAYOFMONTH") { - month = $("select[name=riyearlydayofmonthmonth]", form).val(); - day = $("select[name=riyearlydayofmonthday]", form).val(); - result += `;BYMONTH=${month};BYMONTHDAY=${day}`; - human += `, ${conf.localization.yearlyDayOfMonth1Human} ${ - conf.localization.months[month - 1] - } ${day}`; - } else if (yearlyType === "WEEKDAYOFMONTH") { - index = $("select[name=riyearlyweekdayofmonthindex]", form).val(); - day = - conf.weekdays[ - $("select[name=riyearlyweekdayofmonthday]", form).val() - ]; - month = $("select[name=riyearlyweekdayofmonthmonth]", form).val(); - result += `;BYMONTH=${month}`; - if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) { - result += `;BYDAY=${index}${day}`; - human += ", " + conf.localization.yearlyWeekdayOfMonth1Human; - human += - " " + - conf.localization.orderIndexes[ - $.inArray(index, conf.orderIndexes) - ]; - human += " " + conf.localization.yearlyWeekdayOfMonth2; - human += - " " + - conf.localization.weekdays[$.inArray(day, conf.weekdays)]; - human += " " + conf.localization.yearlyWeekdayOfMonth3; - human += " " + conf.localization.months[month - 1]; - human += " " + conf.localization.yearlyWeekdayOfMonth4; - } - } - break; - - case "rirangeoptions": - rangeType = form.find("input[name=rirangetype]:checked").val(); - if (rangeType === "BYOCCURRENCES") { - occurrences = form - .find("input[name=rirangebyoccurrencesvalue]") - .val(); - result += `;COUNT=${occurrences}`; - human += `, ${conf.localization.rangeByOccurrences1Human} ${occurrences} ${conf.localization.rangeByOccurrences2}`; - } else if (rangeType === "BYENDDATE") { - let date = form.find("input[name=rirangebyenddatecalendar]").val(); - if (date === "") { - const today = new Date(); - date = `${today.getFullYear()}-${(today.getMonth() + 1) - .toString() - .padStart(2, "0")}-${today.getDate()}`; - } - result += `;UNTIL=${date.replaceAll("-", "")}T000000${ - tz === true ? "Z" : "" - }`; - human += `, ${conf.localization.rangeByEndDateHuman} `; - var date_parts = date.split("-"); - human += format( - new Date(date_parts[0], date_parts[1] - 1, date_parts[2]), - conf.localization.longDateFormat, - conf, - ); - } - break; - } - } - - if (RDATE.length) { - RDATE.sort(); - let tmp_dates = []; - let tmp_human = []; - - // make sure our additional RDATE dates have the same start time - // XXX: not used, remove if superfluous - //const rdate_time = start_date - // ? `T${start_date.getHours()}:${start_date.getMinutes()}:00` - // : "T00:00:00"; - - for (let rdate of RDATE) { - if (rdate !== "") { - // The values from the input field are ISO8601 "YYYY-MM-DD" - // RFC5545 expects a date or date-time format of e.g. - // "YYYYMMDD". See:‌ - // https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2 - rdate = rdate.replaceAll("-", ""); - // by adding "T000000" the recurrence sequence generator of - // plone.event.recurrence adds the current start time correctly - rdate += rdate.length === 8 ? "T000000" : ""; - rdate += tz ? "Z" : ""; - tmp_dates.push(rdate); - - // human readable RDATE - year = parseInt(rdate.substring(0, 4), 10); - month = parseInt(rdate.substring(4, 6), 10) - 1; // js month. - day = parseInt(rdate.substring(6, 8), 10); - tmp_human.push( - format( - new Date(year, month, day), - conf.localization.longDateFormat, - conf, - ), - ); - } - } - if (tmp_dates.length) { - result += `\nRDATE:${tmp_dates.join(",")}`; - human += `${conf.localization.including} ${tmp_human.join("; ")}`; - } - } - - if (EXDATE.length) { - EXDATE.sort(); - let tmp_dates = []; - let tmp_human = []; - for (let exdate of EXDATE) { - if (exdate !== "") { - // EXDATE values are "YYYYMMDDTHHMMZ" - tmp_dates.push(exdate); - - // human readable EXDATE - year = parseInt(exdate.substring(0, 4), 10); - month = parseInt(exdate.substring(4, 6), 10) - 1; - day = parseInt(exdate.substring(6, 8), 10); - tmp_human.push( - format( - new Date(year, month, day), - conf.localization.longDateFormat, - conf, - ), - ); - } - } - if (tmp_dates.length) { - result += `\nEXDATE:${tmp_dates.join(",")}`; - human += `${conf.localization.except} ${tmp_human.join("; ")}`; - } - } - - return { result: result, description: human }; -} - -function parseLine(icalline) { - var result = {}; - var pos = icalline.indexOf(":"); - var property = icalline.substring(0, pos); - result.value = icalline.substring(pos + 1); - - if (property.indexOf(";") !== -1) { - pos = property.indexOf(";"); - result.parameters = property.substring(pos + 1); - result.property = property.substring(0, pos); - } else { - result.parameters = null; - result.property = property; - } - return result; -} - -function cleanDates(dates) { - // Get rid of timezones - // TODO: We could parse dates and range here, maybe? - var result = []; - - for (const date of dates.split(",")) { - if (date.indexOf("Z") !== -1) { - result.push(date.substring(0, 15)); - } else { - result.push(date); - } - } - return result; -} - -function parseIcal(icaldata) { - var result = { - RRULE: "", - RDATE: [], - EXDATE: [], - }; - var line = null; - var nextline; - - var lines = icaldata.split("\n"); - lines.reverse(); - while (line !== "") { - if (lines.length > 0) { - nextline = lines.pop(); - if (nextline.charAt(0) === " " || nextline.charAt(0) === "\t") { - // Line continuation: - line = line + nextline; - continue; - } - } else { - nextline = ""; - } - - // New line; the current one is finished, add it to the result. - if (line !== null) { - line = parseLine(line); - // We ignore properties for now - if (line.property === "RDATE" || line.property === "EXDATE") { - result[line.property] = cleanDates(line.value); - } else { - result[line.property] = line.value; - } - } - - line = nextline; - } - return result; -} - -function widgetLoadFromRfc5545(form, conf, icaldata, force) { - var unsupportedFeatures = []; - var matches, rtemplate, d, input, index; - var selectors, field, radiobutton; - var freq, interval, byday, bymonth, bymonthday, count, until; - var day, month, year, weekday; - - if (icaldata.RRULE === undefined) { - unsupportedFeatures.push(conf.localization.noRule); - if (!force) { - return -1; // Fail! - } - } else { - matches = /FREQ=([^;]+);?/.exec(icaldata.RRULE); - if (matches) { - freq = matches[1]; - } else { - freq = "DAILY"; - } - - matches = /INTERVAL=([0-9]+);?/.exec(icaldata.RRULE); - if (matches) { - interval = matches[1]; - } else { - interval = "1"; - } - - matches = /BYDAY=([^;]+);?/.exec(icaldata.RRULE); - if (matches) { - byday = matches[1]; - } else { - byday = ""; - } - - matches = /BYMONTHDAY=([^;]+);?/.exec(icaldata.RRULE); - if (matches) { - bymonthday = matches[1].split(","); - } else { - bymonthday = null; - } - - matches = /BYMONTH=([^;]+);?/.exec(icaldata.RRULE); - if (matches) { - bymonth = matches[1].split(","); - } else { - bymonth = null; - } - - matches = /COUNT=([0-9]+);?/.exec(icaldata.RRULE); - if (matches) { - count = matches[1]; - } else { - count = null; - } - - matches = /UNTIL=([0-9T]+);?/.exec(icaldata.RRULE); - if (matches) { - until = matches[1]; - } else { - until = null; - } - - matches = /BYSETPOS=([^;]+);?/.exec(icaldata.RRULE); - if (matches) { - unsupportedFeatures.push(conf.localization.bysetpos); - } - - // Find the best rule: - if (Object.prototype.hasOwnProperty.call(conf.rtemplate, freq.toLowerCase())) { - rtemplate = conf.rtemplate[freq.toLowerCase()]; - } else { - for (freq of conf.rtemplate) { - /* fallback to first available template if no match above*/ - rtemplate = conf.rtemplate[freq]; - break; - } - unsupportedFeatures.push(conf.localization.noTemplateMatch); - } - - // set rirtemplate selector to computed value - form.find("select[name='rirtemplate']").val(freq.toLowerCase()); - - for (const field_id of rtemplate.fields) { - field = form.find(`#${field_id}`); - switch (field.attr("id")) { - case "ridailyinterval": - field.find("input[name=ridailyinterval]").val(interval); - break; - - case "riweeklyinterval": - field.find("input[name=riweeklyinterval]").val(interval); - break; - - case "riweeklyweekdays": - if (byday.length === 0) break; - byday = byday.split(","); - for (d = 0; d < conf.weekdays.length; d++) { - input = field.find(`input[name="riweeklyweekdays${d}"]`); - if (input.length === 0) continue; - day = conf.weekdays[d]; - input.attr("checked", byday.includes(day)); - } - break; - - case "rimonthlyinterval": - field.find("input[name=rimonthlyinterval]").val(interval); - break; - - case "rimonthlyoptions": - var monthlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY - - if (bymonthday) { - monthlyType = "DAYOFMONTH"; - if (bymonthday.length > 1) { - // No support for multiple days in one month - unsupportedFeatures.push( - conf.localization.multipleDayOfMonth, - ); - // Just keep the first - bymonthday = bymonthday[0]; - } - field - .find("select[name=rimonthlydayofmonthday]") - .val(bymonthday); - } - - if (byday) { - monthlyType = "WEEKDAYOFMONTH"; - - if (byday.indexOf(",") !== -1) { - // No support for multiple days in one month - unsupportedFeatures.push( - conf.localization.multipleDayOfMonth, - ); - byday = byday.split(",")[0]; - } - index = byday.slice(0, -2); - if (index.charAt(0) !== "+" && index.charAt(0) !== "-") { - index = "+" + index; - } - weekday = byday.slice(-2); - field - .find("select[name=rimonthlyweekdayofmonthindex]") - .val(index); - const day_index = conf.weekdays.indexOf(weekday); - field - .find("select[name=rimonthlyweekdayofmonth]") - .val(day_index); - } - - selectors = field.find("input[name=rimonthlytype]"); - for (index = 0; index < selectors.length; index++) { - radiobutton = selectors[index]; - $(radiobutton).attr( - "checked", - radiobutton.value === monthlyType, - ); - } - break; - - case "riyearlyinterval": - field.find("input[name=riyearlyinterval]").val(interval); - break; - - case "riyearlyoptions": - var yearlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY - - if (bymonthday) { - yearlyType = "DAYOFMONTH"; - if (bymonthday.length > 1) { - // No support for multiple days in one month - unsupportedFeatures.push( - conf.localization.multipleDayOfMonth, - ); - bymonthday = bymonthday[0]; - } - field.find("select[name=riyearlydayofmonthmonth]").val(bymonth); - field.find("select[name=riyearlydayofmonthday]").val(bymonthday); - } - - if (byday) { - yearlyType = "WEEKDAYOFMONTH"; - - if (byday.indexOf(",") !== -1) { - // No support for multiple days in one month - unsupportedFeatures.push( - conf.localization.multipleDayOfMonth, - ); - byday = byday.split(",")[0]; - } - index = byday.slice(0, -2); - if (index.charAt(0) !== "+" && index.charAt(0) !== "-") { - index = "+" + index; - } - weekday = byday.slice(-2); - field - .find("select[name=riyearlyweekdayofmonthindex]") - .val(index); - - const weekday_index = conf.weekdays.indexOf(weekday); - field - .find("select[name=riyearlyweekdayofmonthday]") - .val(weekday_index); - field - .find("select[name=riyearlyweekdayofmonthmonth]") - .val(bymonth); - } - - selectors = field.find("input[name=riyearlyType]"); - for (index = 0; index < selectors.length; index++) { - radiobutton = selectors[index]; - $(radiobutton).attr("checked", radiobutton.value === yearlyType); - } - break; - - case "rirangeoptions": - // default value per configuration - var rangeType = conf.hasRepeatForeverButton - ? "NOENDDATE" - : "BYOCCURRENCES"; - - if (count) { - rangeType = "BYOCCURRENCES"; - field.find("input[name=rirangebyoccurrencesvalue]").val(count); - } - - if (until) { - rangeType = "BYENDDATE"; - input = field.find("input[name=rirangebyenddatecalendar]"); - year = until.slice(0, 4); - month = until.slice(4, 6); - day = until.slice(6, 8); - input.val(`${year}-${month}-${day}`); - } - - selectors = field.find("input[name=rirangetype]"); - for (index = 0; index < selectors.length; index++) { - radiobutton = selectors[index]; - $(radiobutton).attr("checked", radiobutton.value === rangeType); - } - break; - } - } - } - - var messagearea = form.find("#messagearea"); - if (unsupportedFeatures.length !== 0) { - messagearea.text( - conf.localization.unsupportedFeatures + " " + unsupportedFeatures.join("; "), - ); - messagearea.show(); - return 1; - } else { - messagearea.text(""); - messagearea.hide(); - return 0; - } -} - -const RecurrenceInput = function (conf, textarea) { - var self = this; - var $textarea = $(textarea); - - // initalize parsed icaldata - if (textarea.innerHTML) { - textarea["ical"] = parseIcal(textarea.innerHTML); - } else { - textarea["ical"] = { - RRULE: "", - RDATE: [], - EXDATE: [], - }; - } - - // Extend conf with non-configurable data used by templates. - var orderedWeekdays = []; - var now_date = new Date().toISOString().substring(0, 10); - var index; - - for (let i = 0; i < 7; i++) { - index = i + conf.firstDay; - if (index > 6) { - index = index - 7; - } - orderedWeekdays.push(index); - } - - conf = { - ...conf, - orderIndexes: ["+1", "+2", "+3", "+4", "-1"], - weekdays: ["SU", "MO", "TU", "WE", "TH", "FR", "SA"], - orderedWeekdays: orderedWeekdays, - }; - - // The recurrence type dropdown should show certain fields depending - // on selection: - function displayFields(selector) { - // First hide all the fields - self.$modalForm.find(".rifield").hide(); - // Then show the ones that should be shown. - var value = selector.val(); - if (value) { - for (const rtField of conf.rtemplate[value].fields) { - self.$modalForm.find(`#${rtField}`).show(); - } - } - } - - function occurrenceExclude(event) { - event.preventDefault(); - textarea["ical"].EXDATE.push(this.attributes.date.value); - var $this = $(this); - $this.addClass("exdate"); - $this.parent().parent().addClass("exdate"); - $this.off("click").on("click", occurrenceInclude); - } - - function occurrenceInclude(event) { - event.preventDefault(); - textarea["ical"].EXDATE.splice( - $.inArray(this.attributes.date.value, textarea["ical"].EXDATE), - 1, - ); - var $this = $(this); - $this.removeClass("exdate"); - $this.parent().parent().removeClass("exdate"); - $this.off("click").on("click", occurrenceExclude); - } - - function occurrenceDelete(event) { - event.preventDefault(); - textarea["ical"].RDATE.splice( - $.inArray(this.attributes.date.value, textarea["ical"].RDATE), - 1, - ); - $(this) - .parent() - .parent() - .hide("slow", function () { - $(this).remove(); - }); - } - - function occurrenceAdd(event) { - event.preventDefault(); - var datevalue = self.$modalForm.find(".riaddoccurrence input#adddate").val(); - var date_parts = datevalue.split("-"); - datevalue += datevalue.length === 10 ? "T000000" : ""; - var errorarea = self.$modalForm.find(".riaddoccurrence div.alert"); - errorarea.text(""); - errorarea.hide(); - - // Add date only if it is not already in RDATE - if (!textarea["ical"].RDATE.includes(datevalue)) { - textarea["ical"].RDATE.push(datevalue); - var $newdate = - $(`
- - ${format( - new Date(date_parts[0], date_parts[1] - 1, date_parts[2]), - conf.localization.longDateFormat, - conf, - )}, - ${conf.localization.additionalDate} - - - - -
`); - $newdate.hide(); - self.$modalForm.find("div.rioccurrences").prepend($newdate); - $newdate.slideDown(); - $newdate.find("button.rdate").on("click", occurrenceDelete); - } else { - errorarea.text(conf.localization.alreadyAdded).show(); - } - } - - // element is where to find the tag in question. Can be the form - // or the display widget. Defaults to the self.$modalForm. - function loadOccurrences(startdate, rfc5545, start, readonly) { - var element, occurrenceDiv; - - if (!readonly) { - element = self.$modalForm; - } else { - element = self.display; - } - - occurrenceDiv = element.find(".rioccurrences"); - - const year = startdate.getFullYear(); - const month = startdate.getMonth() + 1; - const day = startdate.getDate(); - - var data = { - year: year, - month: month, // Sending January as 0? I think not. - day: day, - rrule: rfc5545, - format: conf.localization.longDateFormat, - start: start, - }; - - $.ajax({ - url: conf.ajaxURL, - async: false, // Can't be tested if it's asynchronous, annoyingly. - type: "post", - dataType: "json", - contentType: conf.ajaxContentType, - cache: false, - data: data, - success: function (resp) { - var result; - - resp.readOnly = readonly; - resp.localization = conf.localization; - resp.icons = conf.icons; - - // Format dates: - var date, y, m, d; - for (let occurrence of resp.occurrences) { - date = occurrence.date; - y = parseInt(date.substring(0, 4), 10); - m = parseInt(date.substring(4, 6), 10) - 1; // jan=0 - d = parseInt(date.substring(6, 8), 10); - occurrence.date = `${y}-${zeropad(m + 1)}-${zeropad(d)}T000000`; - occurrence.formattedDate = format( - new Date(y, m, d), - conf.localization.longDateFormat, - conf, - ); - } - - result = _.template(OccurrenceTemplate)(resp); - occurrenceDiv.replaceWith(result); - - // Add the batch actions: - element.find(".rioccurrences .batching a").on("click", function (event) { - event.stopPropagation(); - event.preventDefault(); - loadOccurrences( - startdate, - rfc5545, - this.attributes.start.value, - readonly, - ); - }); - - // Add the delete/undelete actions: - if (!readonly) { - element - .find(".rioccurrences .action button.rrule") - .on("click", occurrenceExclude); - element - .find(".rioccurrences .action button.exdate") - .on("click", occurrenceInclude); - element - .find(".rioccurrences .action button.rdate") - .on("click", occurrenceDelete); - } - }, - error: function () { - occurrenceDiv[0].innerHTML = ` -
${conf.localization.error_load_occurrences}
- `; - }, - }); - } - - function getField(field) { - // See if it is a field already - var realField = $(field); - if (!realField.length) { - // Otherwise, we assume it's an id: - realField = $("#" + field); - } - if (!realField.length) { - // Still not? Then it's a name. - realField = $("input[name='" + field + "']"); - } - return realField; - } - function findStartDate() { - var startdate = null; - var startField, startFieldYear, startFieldMonth, startFieldDay; - - // Find the default byday and bymonthday from the start date, if any: - if (conf.startField) { - startField = getField(conf.startField); - if (!startField.length) { - // Field not found - return null; - } - startdate = startField.val(); - if (startdate === "") { - // Probably not an input at all. Try to see if it contains a date - startdate = startField.text(); - } - - if (typeof startdate === "string") { - // convert human readable, non ISO8601 dates, like - // '2014-04-24 19:00', where the 'T' separator is missing. - startdate = startdate.replace(" ", "T"); - } - - startdate = new Date(startdate); - } else if (conf.startFieldYear && conf.startFieldMonth && conf.startFieldDay) { - startFieldYear = getField(conf.startFieldYear); - startFieldMonth = getField(conf.startFieldMonth); - startFieldDay = getField(conf.startFieldDay); - if ( - !startFieldYear.length && - !startFieldMonth.length && - !startFieldDay.length - ) { - // Field not found - return null; - } - startdate = new Date( - startFieldYear.val(), - startFieldMonth.val() - 1, - startFieldDay.val(), - ); - } - if (startdate === null) { - return null; - } - // We have some sort of startdate: - if (isNaN(startdate)) { - return null; - } - return startdate; - } - function findEndDate() { - var endField, enddate; - - endField = self.$modalForm.find("input[name=rirangebyenddatecalendar]"); - enddate = endField.val(); - enddate = new Date(enddate); - - // if the end date is incorrect or the field is left empty - if (isNaN(enddate) || endField.val() === "") { - return null; - } - return enddate; - } - function findIntField(fieldName) { - var field, num; - - field = self.$modalForm.find("input[name=" + fieldName + "]"); - - num = field.val(); - - // if it's not a number or the field is left empty - if (isNaN(num) || num.toString().indexOf(".") !== -1 || field.val() === "") { - return null; - } - return num; - } - - // Loading (populating) display and form widget with - // passed RFC5545 string (data) - function loadData(form) { - widgetLoadFromRfc5545(form, conf, textarea["ical"], true); - - const startdate = findStartDate(); - - if (startdate !== null) { - // If the date is a real date, set the defaults in the form - document.querySelector("select[name=rimonthlydayofmonthday]").value = - startdate.getDate(); - const dayindex = - conf.orderIndexes[Math.floor((startdate.getDate() - 1) / 7)]; - const day = conf.weekdays[startdate.getDay()]; - document.querySelector("select[name=rimonthlyweekdayofmonthindex]").value = - dayindex; - document.querySelector("select[name=rimonthlyweekdayofmonth]").value = day; - - document.querySelector("select[name=riyearlydayofmonthmonth]").value = - startdate.getMonth() + 1; - document.querySelector("select[name=riyearlydayofmonthday]").value = - startdate.getDate(); - document.querySelector("select[name=riyearlyweekdayofmonthindex]").value = - dayindex; - document.querySelector("select[name=riyearlyweekdayofmonthday]").value = day; - document.querySelector("select[name=riyearlyweekdayofmonthmonth]").value = - startdate.getMonth() + 1; - - // Now when we have a start date, we can also do an ajax call to calculate occurrences: - var rfc5545 = - textarea.innerHTML || - widgetSaveToRfc5545( - form, - textarea["ical"].RDATE, - textarea["ical"].EXDATE, - conf, - false, - ).result; - - loadOccurrences(startdate, rfc5545, 0, false); - - // Show the add and refresh buttons: - document.querySelector("div.rioccurrencesactions").style.display = "block"; - } else { - // No EXDATE/RDATE support - document.querySelector("div.rioccurrencesactions").style.display = "none"; - } - - displayFields(form.find("select[name=rirtemplate]")); - } - - function recurrenceOn(form) { - var RFC5545 = widgetSaveToRfc5545( - form, - textarea["ical"].RDATE, - textarea["ical"].EXDATE, - conf, - false, - ); - var label = self.display.find("label[class=ridisplay-label]"); - label.text(conf.localization.displayActivate + " " + RFC5545.description); - textarea["ical"] = parseIcal(RFC5545.result); - textarea.innerHTML = RFC5545.result; - $textarea.trigger("change"); - var startdate = findStartDate(); - if (startdate !== null) { - loadOccurrences(startdate, RFC5545.result, 0, true); - } - self.display.find('button[name="riedit"]').text(conf.localization.edit_rules); - self.display.find('button[name="ridelete"]').show(); - } - - function recurrenceOff() { - var label = self.display.find("label[class=ridisplay-label]"); - label.text(conf.localization.displayUnactivate); - // reset ical object - textarea["ical"] = { - RRULE: "", - RDATE: [], - EXDATE: [], - }; - textarea.innerHTML = ""; - $textarea.trigger("change"); // Clear the textarea. - self.display.find(".rioccurrences").hide(); - self.display.find('button[name="riedit"]').text(conf.localization.add_rules); - self.display.find('button[name="ridelete"]').hide(); - } - - function checkFields(form) { - var startDate, endDate, num, messagearea; - startDate = findStartDate(); - - // Hide any error message from before - messagearea = self.$modalForm.find("#messagearea"); - messagearea.text(""); - messagearea.hide(); - - // Hide add field errors - self.$modalForm.find(".riaddoccurrence div.alert").text("").hide(); - - // Repeats Daily - if (self.$modalForm.find("#ridailyinterval").css("display") === "block") { - // Check repeat every field - num = findIntField("ridailyinterval", form); - if (!num || num < 1 || num > 1000) { - messagearea.text(conf.localization.noRepeatEvery).show(); - return false; - } - } - - // Repeats Weekly - if (self.$modalForm.find("#riweeklyinterval").css("display") === "block") { - // Check repeat every field - num = findIntField("riweeklyinterval", form); - if (!num || num < 1 || num > 1000) { - messagearea.text(conf.localization.noRepeatEvery).show(); - return false; - } - } - - // Repeats Monthly - if (self.$modalForm.find("#rimonthlyinterval").css("display") === "block") { - // Check repeat every field - num = findIntField("rimonthlyinterval", form); - if (!num || num < 1 || num > 1000) { - messagearea.text(conf.localization.noRepeatEvery).show(); - return false; - } - - // Check repeat on - if (self.$modalForm.find("#rimonthlyoptions input:checked").length === 0) { - messagearea.text(conf.localization.noRepeatOn).show(); - return false; - } - } - - // Repeats Yearly - if (self.$modalForm.find("#riyearlyinterval").css("display") === "block") { - // Check repeat every field - num = findIntField("riyearlyinterval", form); - if (!num || num < 1 || num > 1000) { - messagearea.text(conf.localization.noRepeatEvery).show(); - return false; - } - - // Check repeat on - if (self.$modalForm.find("#riyearlyoptions input:checked").length === 0) { - messagearea.text(conf.localization.noRepeatOn).show(); - return false; - } - } - - // End recurrence fields - - // If after N occurences is selected, check its value - if ( - self.$modalForm.find('input[value="BYOCCURRENCES"]:visible:checked').length > - 0 - ) { - num = findIntField("rirangebyoccurrencesvalue", form); - if (!num || num < 1 || num > 1000) { - messagearea.text(conf.localization.noEndAfterNOccurrences).show(); - return false; - } - } - - // If end date is selected, check its value - if ( - self.$modalForm.find('input[value="BYENDDATE"]:visible:checked').length > 0 - ) { - endDate = findEndDate(form); - if (!endDate) { - // if endDate is null that means the field is empty - messagearea.text(conf.localization.noEndDate).show(); - return false; - } else if (endDate < startDate) { - // the end date cannot be before start date - messagearea.text(conf.localization.pastEndDate).show(); - return false; - } - } - - return true; - } - - function save(event) { - event.preventDefault(); - // if no field errors, process the request - if (checkFields(self.$modalForm)) { - // close modal - self.modal.hide(); - recurrenceOn(self.$modalForm); - self.$modalForm = null; - } - } - - function cancel(event) { - event.preventDefault(); - // close modal - self.modal.hide(); - self.$modalForm = null; - } - - function updateOccurrences() { - var startDate; - startDate = findStartDate(); - - // if no field errors, process the request - if (checkFields(form)) { - loadOccurrences( - startDate, - widgetSaveToRfc5545( - self.$modalForm, - textarea["ical"].RDATE, - textarea["ical"].EXDATE, - conf, - false, - ).result, - 0, - false, - ); - } - } - - function initModalForm() { - var enddate = self.$modalForm.find("input[name=rirangebyenddatecalendar]"); - if (!enddate.val()) { - var start_date = findStartDate(); - start_date.setDate(start_date.getDate() + 1); - enddate.val(start_date.toISOString().substring(0, 10)); - } - - self.$modalForm.find("input#addaction").on("click", occurrenceAdd); - - // When selecting template, update what fieldsets are visible and the occurrences. - self.$modalForm.find('select[name="rirtemplate"]').on("change", function () { - displayFields($(this)); - updateOccurrences(); - }); - - // Update the selected dates section - self.$modalForm - .find( - ` - input:radio, - input:checkbox, - .riweeklyweekday > input, - input[name=ridailyinterval], - input[name=riweeklyinterval], - input[name=rimonthlyinterval], - input[name=riyearlyinterval], - input[name=rirangebyoccurrencesvalue], - input[name=rirangebyenddatecalendar], - #rimonthlyoptions select, - #riyearlyoptions select, - #riyearlyinterval select`, - ) - .on("change", function () { - // Update only if the occurances are shown - if (self.$modalForm.find(".rioccurrencesactions:visible").length !== 0) { - updateOccurrences(); - } - }); - - // initialize occurrence adddate value - self.$modalForm.find("div.riaddoccurrence input#adddate").val(now_date); - - /* - Save and cancel methods: - */ - self.$modalForm.find(".ricancelbutton").on("click", cancel); - self.$modalForm.find(".risavebutton").on("click", save); - } - - /* - Load the templates - */ - - self.display = $(_.template(DisplayTemplate)(conf)); - var form = $(_.template(FormTemplate)(conf)); - - form.appendTo("body"); - form.hide(); - - // Make an modal and hide it - self.modal = new Modal(form, { - content: "div.riform", - buttons: ".ributtons > button", - modalSizeClass: "modal-lg", - backdropOptions: { - opacity: "0.5", - }, - }); - - if (textarea.innerHTML) { - var result = widgetLoadFromRfc5545(form, conf, textarea["ical"], false); - if (result === -1) { - var label = self.display.find("label[class=ridisplay-label]"); - label.text(conf.localization.noRule); - } else { - recurrenceOn(form); - } - } - - /* - Do all the GUI stuff: - */ - - // When you click "Delete...", the recurrence rules should be cleared. - self.display.find('button[name="ridelete"]').on("click", function (e) { - e.preventDefault(); - recurrenceOff(); - }); - - // Show form modal when you click on the "Edit..." link - self.display.find('button[name="riedit"]').on("click", function (e) { - // Load the form to set up the right fields to show, etc. - e.preventDefault(); - self.modal.show(); - self.$modalForm = $("form", self.modal.$modalContent); - loadData(self.$modalForm); - initModalForm(); - }); -}; +// Thin registration module: the implementation (incl. the XML templates and +// the Modal dependency) is loaded lazily on first match, so it stays out of +// the eager patterns chunk on pages without a recurrence widget. export default Base.extend({ name: "recurrence", trigger: ".pat-recurrence", parser: "mockup", - defaults: { - localization: { - displayUnactivate: "Does not repeat", - displayActivate: "Repeats every", - add_rules: "Add", - edit_rules: "Edit", - delete_rules: "Delete", - add: "Add", - refresh: "Refresh", - - title: "Repeat", - preview: "Selected dates", - addDate: "Add date", - - recurrenceType: "Repeats:", - - dailyInterval1: "Repeat every:", - dailyInterval2: "days", - - weeklyInterval1: "Repeat every:", - weeklyInterval2: "week(s)", - weeklyWeekdays: "Repeat on:", - weeklyWeekdaysHuman: "on:", - - monthlyInterval1: "Repeat every:", - monthlyInterval2: "month(s)", - monthlyDayOfMonth1: "Day", - monthlyDayOfMonth1Human: "on day", - monthlyDayOfMonth2: "of the month", - monthlyDayOfMonth3: "month(s)", - monthlyWeekdayOfMonth1: "The", - monthlyWeekdayOfMonth1Human: "on the", - monthlyWeekdayOfMonth2: "", - monthlyWeekdayOfMonth3: "of the month", - monthlyRepeatOn: "Repeat on:", - - yearlyInterval1: "Repeat every:", - yearlyInterval2: "year(s)", - yearlyDayOfMonth1: "Every", - yearlyDayOfMonth1Human: "on", - yearlyDayOfMonth2: "", - yearlyDayOfMonth3: "", - yearlyWeekdayOfMonth1: "The", - yearlyWeekdayOfMonth1Human: "on the", - yearlyWeekdayOfMonth2: "", - yearlyWeekdayOfMonth3: "of", - yearlyWeekdayOfMonth4: "", - yearlyRepeatOn: "Repeat on:", - - range: "End recurrence:", - rangeNoEnd: "Never", - rangeByOccurrences1: "After", - rangeByOccurrences1Human: "ends after", - rangeByOccurrences2: "occurrence(s)", - rangeByEndDate: "On", - rangeByEndDateHuman: "ends on", - - including: ", and also", - except: ", except for", - - cancel: "Cancel", - save: "Save", - - recurrenceStart: "Start of the recurrence", - additionalDate: "Additional date", - include: "Include", - exclude: "Exclude", - remove: "Remove", - - orderIndexes: ["first", "second", "third", "fourth", "last"], - months: [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ], - shortMonths: [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ], - weekdays: [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ], - shortWeekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], - - longDateFormat: "mmmm dd, yyyy", - shortDateFormat: "mm/dd/yyyy", - - unsupportedFeatures: - "Warning: This event uses recurrence features not " + - "supported by this widget. Saving the recurrence " + - "may change the recurrence in unintended ways:", - noTemplateMatch: "No matching recurrence template", - multipleDayOfMonth: - "This widget does not support multiple days in monthly or yearly recurrence", - bysetpos: "BYSETPOS is not supported", - noRule: "No RRULE in RRULE data", - noRepeatEvery: 'Error: The "Repeat every"-field must be between 1 and 1000', - noEndDate: "Error: End date is not set. Please set a correct value", - noRepeatOn: 'Error: "Repeat on"-value must be selected', - pastEndDate: "Error: End date cannot be before start date", - noEndAfterNOccurrences: - 'Error: The "After N occurrences"-field must be between 1 and 1000', - alreadyAdded: "This date was already added", - - rtemplate: { - daily: "Daily", - mondayfriday: "Monday and Friday", - weekdays: "Weekday", - weekly: "Weekly", - monthly: "Monthly", - yearly: "Yearly", - }, - - error_load_occurrences: "Cannot load the occurrences preview.", - }, - - readOnly: false, - firstDay: 0, - - // "REMOTE" FIELD - startField: null, - startFieldYear: null, - startFieldMonth: null, - startFieldDay: null, - ajaxURL: null, - ajaxContentType: "application/json; charset=utf8", - ributtonExtraClass: "", - - // INPUT CONFIGURATION - allowAdditionalDates: false, - hasRepeatForeverButton: true, - - // JQUERY TEMPLATE NAMES - template: { - form: "#jquery-recurrenceinput-form-tmpl", - display: "#jquery-recurrenceinput-display-tmpl", - }, - - // RECURRENCE TEMPLATES - rtemplate: { - daily: { - rrule: "FREQ=DAILY", - fields: ["ridailyinterval", "rirangeoptions"], - }, - mondayfriday: { - rrule: "FREQ=WEEKLY;BYDAY=MO,FR", - fields: ["rirangeoptions"], - }, - weekdays: { - rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", - fields: ["rirangeoptions"], - }, - weekly: { - rrule: "FREQ=WEEKLY", - fields: ["riweeklyinterval", "riweeklyweekdays", "rirangeoptions"], - }, - monthly: { - rrule: "FREQ=MONTHLY", - fields: ["rimonthlyinterval", "rimonthlyoptions", "rirangeoptions"], - }, - yearly: { - rrule: "FREQ=YEARLY", - fields: ["riyearlyinterval", "riyearlyoptions", "rirangeoptions"], - }, - }, - }, - - load_icons: async function () { - return { - reload: await utils.resolveIcon("arrow-clockwise"), - remove: await utils.resolveIcon("trash"), - exclude: await utils.resolveIcon("calendar-x"), - include: await utils.resolveIcon("plus-circle"), - }; - }, init: async function () { - // tmpl BEFORE recurrenceinput - import("./recurrence.scss"); - - this.el.classList.add("recurrence-widget"); - var icons = await this.load_icons(); - - var config = { - ...this.options, - ...this.options.configuration, - icons: icons, - }; - - // our recurrenceinput widget instance - var recurrenceinput = new RecurrenceInput(config, this.el); - // hide textarea and place display widget after textarea - this.$el.after(recurrenceinput.display); - - // hide the textarea - this.el.style.display = "none"; + const impl = (await import("./recurrence--implementation")).default; + // The options were parsed against empty defaults — the defaults live + // with the implementation. Merge them in now, keeping parsed values. + this.options = $.extend(true, {}, impl.defaults, this.options); + // Graft the implementation onto this instance and run its init. + $.extend(this, impl); + return impl.init.apply(this, arguments); }, });