UNPKG

@progress/kendo-ui

Version:

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

1,298 lines (1,297 loc) 155 kB
//#region ../src/kendo.scheduler.js const __meta__ = { id: "scheduler", name: "Scheduler", category: "web", description: "The Scheduler is an event calendar.", depends: [ "dropdownlist", "editable", "multiselect", "window", "datepicker", "datetimepicker", "toolbar", "scheduler.recurrence", "scheduler.view", "html.button", "icons" ], features: [ { id: "scheduler-dayview", name: "Scheduler Day View", description: "Scheduler Day View", depends: ["scheduler.dayview"] }, { id: "scheduler-agendaview", name: "Scheduler Agenda View", description: "Scheduler Agenda View", depends: ["scheduler.agendaview"] }, { id: "scheduler-monthview", name: "Scheduler Month View", description: "Scheduler Month View", depends: ["scheduler.monthview"] }, { id: "scheduler-timelineview", name: "Scheduler Timeline View", description: "Scheduler Timeline View", depends: ["scheduler.timelineview"] }, { id: "scheduler-yearview", name: "Scheduler Year View", description: "Scheduler Year View", depends: ["scheduler.yearview"] }, { id: "scheduler-mobile", name: "Scheduler adaptive rendering", description: "Support for adaptive rendering", depends: [ "dialog", "pane", "switch" ] }, { id: "scheduler-pdf-export", name: "PDF export", description: "Export the scheduler events as PDF", depends: ["pdf", "drawing"] }, { id: "scheduler-timezones", name: "Timezones", description: "Allow selecting timezones different than Etc/UTC", depends: ["timezones"] } ] }; (function($, undefined) { var kendo = window.kendo, date = kendo.date, MS_PER_DAY = date.MS_PER_DAY, getDate = date.getDate, getMilliseconds = kendo.date.getMilliseconds, recurrence = kendo.recurrence, encode = kendo.htmlEncode, keys = kendo.keys, ui = kendo.ui, Widget = ui.Widget, DataBoundWidget = ui.DataBoundWidget, STRING = "string", Popup = ui.Popup, Calendar = ui.Calendar, DataSource = kendo.data.DataSource, isPlainObject = $.isPlainObject, extend = $.extend, toString = Object.prototype.toString, isArray = Array.isArray, NS = ".kendoScheduler", CLICK = "click", MOUSEDOWN = "mousedown", TOUCHSTART = kendo.support.pointers ? "pointerdown" : "touchstart", TOUCHMOVE = kendo.support.pointers ? "pointermove" : "touchmove", TOUCHEND = kendo.support.pointers ? "pointerup" : "touchend", MOUSEMOVE = kendo.support.mousemove, CHANGE = "change", PROGRESS = "progress", ERROR = "error", CANCEL = "cancel", REMOVE = "remove", RESET = "resetSeries", SAVE = "save", ADD = "add", EDIT = "edit", DISABLED = "disabled", OPTION = "option", FOCUSEDSTATE = "k-focus", INVERSECOLORCLASS = "k-event-inverse", valueStartEndBoundRegex = /(?:value:start|value:end)(?:,|$)/, MIN_SCREEN = "(min-width: 1024px)", TODAY = getDate(new Date()), EXCEPTION_SEPARATOR = ",", OLD_EXCEPTION_SEPARATOR_REGEXP = /\;/g, RECURRENCE_EXCEPTION = "recurrenceException", DELETECONFIRM = "Are you sure you want to delete this event?", DELETERECURRING = "Do you want to delete only this event occurrence or the whole series?", EDITRECURRING = "Do you want to edit only this event occurrence or the whole series?", DELETERECURRINGCONFIRM = "Are you sure you want to delete this event occurrence?", RESETSERIESCONFIRM = "Are you sure you want to reset the whole series?", DELETESERIESCONFIRM = "Are you sure you want to delete the whole series?", ONGOING_CLASS = "k-event-ongoing", COMMANDBUTTONTMPL = ({ className, attr, text, icon, fillMode, themeColor }) => kendo.html.renderButton(`<button type="button" class="${className}" ${attr}>${text}</button>`, { icon, fillMode, themeColor }), VIEWS_DROPDOWN_TEMPLATE = kendo.template(({ label, views, type }) => `<select aria-label="${label}" class="k-picker k-dropdown-list k-dropdown ${type}">` + Object.keys(views).map((view) => `<option value="${view}">${views[view].title}</option>`).join("") + "</select>"), DEFAULT_TOOLS = { pdf: { name: "pdf", type: "button", icon: "file-pdf", attributes: { class: "k-pdf" } }, pdfMobile: { name: "pdf", type: "button", icon: "file-pdf", showText: "overflow", attributes: { class: "k-pdf" } }, today: { name: "today", type: "button", attributes: { "ref-nav-today": "" } }, previous: { name: "previous", type: "button", icon: "caret-alt-left", showText: "overflow", attributes: { "ref-nav-prev": "" }, groupClass: "k-scheduler-navigation" }, next: { name: "next", type: "button", icon: "caret-alt-right", showText: "overflow", attributes: { "ref-nav-next": "" }, groupClass: "k-scheduler-navigation" }, current: { name: "current", type: "button", icon: "calendar", fillMode: "flat", text: "placeholder", attributes: { "aria-live": "polite", class: "k-nav-current" } }, search: { template: "<span class=\"k-scheduler-search k-textbox k-input\">" + "<input tabindex=\"-1\" autocomplete=\"off\" class=\"k-input-inner k-scheduler-search-input k-input-inner\" />" + `<span class="k-input-suffix">${kendo.ui.icon("search")}</span>` + "</span>" }, refresh: { name: "refresh", type: "button", icon: "arrow-rotate-cw", showText: "overflow", attributes: { class: "k-scheduler-refresh" } }, create: { name: "create", type: "button", icon: "plus", attributes: { class: "k-create-event" } }, calendar: { name: "calendar", type: "button", icon: "calendar", attributes: { class: "k-nav-calendar" } }, previousMobile: { name: "previous", type: "button", icon: "chevron-left", showText: "overflow", attributes: { "ref-nav-prev": "" }, groupClass: "k-scheduler-navigation" }, nextMobile: { name: "next", type: "button", icon: "chevron-right", showText: "overflow", attributes: { "ref-nav-next": "" }, groupClass: "k-scheduler-navigation" }, currentMobile: { template: "<span class=\"k-scheduler-navigation\">" + "<span class=\"k-nav-current\">" + "<span class=\"k-m-date-format\"></span>" + "<span class=\"k-y-date-format\"></span>" + "</span>" + "</span>" }, view: { name: "view", type: "button", togglable: true, group: "views" } }, defaultDesktopTools = [ [ "today", "previous", "next" ], "current", { type: "spacer" } ], defaultMobileToolsFirst = [["calendar"], { type: "spacer" }], defaultMobileToolsSecond = [ "previousMobile", { type: "spacer" }, "currentMobile", { type: "spacer" }, "nextMobile" ], MOBILEDATERANGEEDITOR = function(container, options) { var attr = { name: options.field, title: options.title }; var isAllDay = options.model.isAllDay; var dateTimeValidate = kendo.attr("validate") + "='" + !isAllDay + "'"; var dateValidate = kendo.attr("validate") + "='" + !!isAllDay + "'"; appendTimezoneAttr(attr, options); appendValidDateValidator(attr, options); appendDateCompareValidator(attr, options); $("<input type=\"datetime-local\" required " + kendo.attr("type") + "=\"datetime-local\" " + kendo.attr("bind") + "=\"value:" + options.field + ", invisible:isAllDay\" " + dateTimeValidate + "/>").attr(attr).appendTo(container); $("<input type=\"date\" required " + kendo.attr("type") + "=\"date\" " + kendo.attr("bind") + "=\"value:" + options.field + ",visible:isAllDay\" " + dateValidate + "/>").attr(attr).appendTo(container); $("<span " + kendo.attr("for") + "=\"" + options.field + "\" class=\"k-invalid-msg\"/>").hide().appendTo(container); }, DATERANGEEDITOR = function(container, options) { var attr = { name: options.field, title: options.title }, isAllDay = options.model.isAllDay, dateTimeValidate = kendo.attr("validate") + "='" + !isAllDay + "' ", dateValidate = kendo.attr("validate") + "='" + !!isAllDay + "' "; appendTimezoneAttr(attr, options); appendValidDateValidator(attr, options); appendDateCompareValidator(attr, options); $("<input type=\"text\" required " + kendo.attr("type") + "=\"date\"" + " " + kendo.attr("role") + "=\"datetimepicker\" " + kendo.attr("bind") + "=\"value:" + options.field + ",invisible:isAllDay\" " + dateTimeValidate + "/>").attr(attr).appendTo(container); $("<input type=\"text\" required " + kendo.attr("type") + "=\"date\"" + " " + kendo.attr("role") + "=\"datepicker\" " + kendo.attr("bind") + "=\"value:" + options.field + ",visible:isAllDay\" " + dateValidate + "/>").attr(attr).appendTo(container); $("<span " + kendo.attr("bind") + "=\"text: " + options.field + "Timezone\"></span>").appendTo(container); if (options.field === "end") { $("<span " + kendo.attr("bind") + "=\"text: startTimezone, invisible: endTimezone\"></span>").appendTo(container); } $("<span " + kendo.attr("for") + "=\"" + options.field + "\" class=\"k-invalid-msg\"/>").hide().appendTo(container); }, RECURRENCEEDITOR = function(container, options) { $("<div " + kendo.attr("bind") + "=\"value:" + options.field + "\" />").attr({ name: options.field }).appendTo(container).kendoRecurrenceEditor({ start: options.model.start, timezone: options.timezone, messages: options.messages }); }, MOBILERECURRENCEEDITOR = function(container, options) { $("<div " + kendo.attr("bind") + "=\"value:" + options.field + "\" />").attr({ name: options.field }).appendTo(container).kendoMobileRecurrenceEditor({ start: options.model.start, timezone: options.timezone, messages: options.messages, pane: options.pane, value: options.model[options.field] }); }, MOBILEISALLDAYEDITOR = function(container, options) { $("<input type=\"checkbox\" data-role=\"switch\"" + kendo.attr("bind") + "=\"value:" + options.field + "\" />").appendTo(container); }, ISALLDAYEDITOR = function(container, options) { $("<input type=\"checkbox\" data-role=\"checkbox\"" + kendo.attr("bind") + "=\"value:" + options.field + "\" data-label=\"" + options.title + "\" />").attr({ id: options.field, name: options.field, title: options.title ? options.title : options.field }).appendTo(container); }, MOBILETIMEZONEPOPUP = function(container, options) { var text = timezoneButtonText(options.model, options.messages.noTimezone); $("<span class=\"k-timezone-label\"></span>").text(text).appendTo(container); $(kendo.ui.icon("arrow-chevron-right")).appendTo(container); container.closest("li.k-item label").on(CLICK, options.click); }, TIMEZONEPOPUP = function(container, options) { $("<a href=\"#\" class=\"k-button\" data-bind=\"invisible:isAllDay\"><span class=\"k-button-text\">" + options.messages.timezoneEditorButton + "</span></a>").on(CLICK, options.click).appendTo(container); }, MOBILETIMEZONEEDITOR = function(container, options) { $("<div class=\"k-mobiletimezoneeditor\" " + kendo.attr("bind") + "=\"value:" + options.field + "\" />").attr({ name: options.field }).appendTo(container).kendoMobileTimezoneEditor({ optionLabel: options.noTimezone }); }, TIMEZONEEDITOR = function(container, options) { var visible = options.visible || options.visible === undefined; $("<div " + kendo.attr("bind") + "=\"value:" + options.field + "\" />").attr({ name: options.field }).toggle(visible).appendTo(container).kendoTimezoneEditor({ optionLabel: options.noTimezone, title: options.title }); }; function timezoneButtonText(model, message) { message = message || ""; if (model.startTimezone) { message = model.startTimezone; if (model.endTimezone) { message += " | " + model.endTimezone; } } return message; } function appendTimezoneAttr(attrs, options) { var timezone = options.timezone; if (timezone) { attrs[kendo.attr("timezone")] = timezone; } } function appendValidDateValidator(attrs, options) { var validationRules = options.model.fields[options.field].validation; if (validationRules) { var validDateRule = validationRules.validDateValidator; if (validDateRule && isPlainObject(validDateRule) && validDateRule.message) { attrs[kendo.attr("validDate-msg")] = validDateRule.message; } } } function appendDateCompareValidator(attrs, options) { var validationRules = options.model.fields[options.field].validation; if (validationRules) { var dateCompareRule = validationRules.dateCompare; if (dateCompareRule && isPlainObject(dateCompareRule) && dateCompareRule.message) { attrs[kendo.attr("dateCompare-msg")] = dateCompareRule.message; } } } function wrapDataAccess(originalFunction, timezone) { return function(data) { data = originalFunction(data); convertData(data, "apply", timezone); return data || []; }; } function wrapDataSerialization(originalFunction, timezone) { return function(data) { if (data) { if (toString.call(data) !== "[object Array]" && !(data instanceof kendo.data.ObservableArray)) { data = [data]; } } convertData(data, "remove", timezone, true); data = originalFunction(data); return data || []; }; } function convertData(data, method, timezone, removeUid) { var event, idx, length, startOffset, endOffset; data = data || []; for (idx = 0, length = data.length; idx < length; idx++) { event = data[idx]; startOffset = event.start ? event.start.getTimezoneOffset() : null; endOffset = event.start ? event.end.getTimezoneOffset() : null; if (removeUid) { if (event.startTimezone || event.endTimezone) { if (timezone) { event.start = kendo.timezone.convert(event.start, event.startTimezone || event.endTimezone, timezone); event.end = kendo.timezone.convert(event.end, event.endTimezone || event.startTimezone, timezone); event.start = kendo.timezone[method](event.start, timezone); event.end = kendo.timezone[method](event.end, timezone); } else { event.start = kendo.timezone[method](event.start, event.startTimezone || event.endTimezone); event.end = kendo.timezone[method](event.end, event.endTimezone || event.startTimezone); } } else if (timezone) { event.start = kendo.timezone[method](event.start, timezone); event.end = kendo.timezone[method](event.end, timezone); } } else { if (event.startTimezone || event.endTimezone) { event.start = kendo.timezone[method](event.start, event.startTimezone || event.endTimezone); event.end = kendo.timezone[method](event.end, event.endTimezone || event.startTimezone); if (timezone) { event.start = kendo.timezone.convert(event.start, event.startTimezone || event.endTimezone, timezone); event.end = kendo.timezone.convert(event.end, event.endTimezone || event.startTimezone, timezone); } } else if (timezone) { event.start = kendo.timezone[method](event.start, timezone); event.end = kendo.timezone[method](event.end, timezone); } } if (removeUid) { delete event.uid; } if (method === "remove" && event.start && startOffset && startOffset !== event.start.getTimezoneOffset()) { event.start = new Date(event.start.getTime() + (startOffset - event.start.getTimezoneOffset()) * 6e4); } if (method === "remove" && event.end && endOffset && endOffset !== event.end.getTimezoneOffset()) { event.end = new Date(event.end.getTime() + (endOffset - event.end.getTimezoneOffset()) * 6e4); } } return data; } function getOccurrenceByUid(data, uid) { var length = data.length, idx = 0, event; for (; idx < length; idx++) { event = data[idx]; if (event.uid === uid) { return event; } } } var SchedulerDataReader = kendo.Class.extend({ init: function(schema, reader) { var timezone = schema.timezone; this.reader = reader; if (reader.model) { this.model = reader.model; } this.timezone = timezone; this.data = wrapDataAccess(this.data.bind(this), timezone); this.serialize = wrapDataSerialization(this.serialize.bind(this), timezone); }, errors: function(data) { return this.reader.errors(data); }, parse: function(data) { return this.reader.parse(data); }, data: function(data) { return this.reader.data(data); }, total: function(data) { return this.reader.total(data); }, groups: function(data) { return this.reader.groups(data); }, aggregates: function(data) { return this.reader.aggregates(data); }, serialize: function(data) { return this.reader.serialize(data); } }); function applyZone(date, fromZone, toZone) { if (toZone) { date = kendo.timezone.convert(date, fromZone, toZone); } else { date = kendo.timezone.remove(date, fromZone); } return date; } function validDateValidator(input) { if (input.filter("[name=start]").length && input.filter("[title=Start]").length || input.filter("[name=end]").length && input.filter("[title=End]").length || input.filter(".k-recur-until").length) { var date; var picker = kendo.widgetInstance(input, kendo.ui); if (picker) { date = kendo.parseDate(input.val(), picker.options.format); return !!date && picker.value(); } else { date = kendo.parseDate(input.val()); return !!date; } } return true; } function dateCompareValidator(input) { if (input.filter("[name=end]").length) { var container = input.closest(".k-scheduler-edit-form"); var startInput = container.find("[name=start]:visible"); var endInput = container.find("[name=end]:visible"); if (endInput[0] && startInput[0]) { var start, end; var startPicker = kendo.widgetInstance(startInput, kendo.ui); var endPicker = kendo.widgetInstance(endInput, kendo.ui); var editable = container.data("kendoEditable"); var model = editable ? editable.options.model : null; if (startPicker && endPicker) { start = startPicker.value(); end = endPicker.value(); } else { start = kendo.parseDate(startInput.val()); end = kendo.parseDate(endInput.val()); } if (start && end) { if (model) { var timezone = startInput.attr(kendo.attr("timezone")); var startTimezone = model.startTimezone; var endTimezone = model.endTimezone; startTimezone = startTimezone || endTimezone; endTimezone = endTimezone || startTimezone; if (startTimezone) { start = applyZone(start, startTimezone, timezone); end = applyZone(end, endTimezone, timezone); } } return start <= end; } } } return true; } function untilDateCompareValidator(input) { var untilPicker, until, container, startInput, start, startPicker; if (input.filter(".k-recur-until").length) { untilPicker = kendo.widgetInstance(input, kendo.ui); until = untilPicker.value(); container = input.closest(".k-scheduler-edit-form"); startInput = container.find("[name=start]:visible"); if (startInput[0]) { startPicker = kendo.widgetInstance(startInput, kendo.ui); if (startPicker) { start = startPicker.value(); } else { start = kendo.parseDate(startInput.val()); } if (start && until) { return start <= until; } } } return true; } var SchedulerEvent = kendo.data.Model.define({ init: function(value) { var that = this; kendo.data.Model.fn.init.call(that, value); that._defaultId = that.defaults[that.idField]; }, _time: function(field) { var date = this[field]; var fieldTime = "_" + field + "Time"; if (this[fieldTime]) { return this[fieldTime] - kendo.date.toUtcTime(kendo.date.getDate(date)); } return getMilliseconds(date); }, _date: function(field) { var fieldTime = "_" + field + "Time"; if (this[fieldTime]) { return this[fieldTime] - this._time(field); } return kendo.date.getDate(this[field]); }, clone: function(options, updateUid) { var uid = this.uid, event = new this.constructor($.extend({}, this.toJSON(), options)); if (!updateUid) { event.uid = uid; } return event; }, duration: function() { var end = this.end; var start = this.start; var offset = (end.getTimezoneOffset() - start.getTimezoneOffset()) * kendo.date.MS_PER_MINUTE; return end - start - offset; }, expand: function(start, end, zone) { return recurrence ? recurrence.expand(this, start, end, zone) : [this]; }, update: function(eventInfo) { for (var field in eventInfo) { this.set(field, eventInfo[field]); } if (this._startTime) { this.set("_startTime", kendo.date.toUtcTime(this.start)); } if (this._endTime) { this.set("_endTime", kendo.date.toUtcTime(this.end)); } }, isMultiDay: function() { return this.isAllDay || this.duration() >= kendo.date.MS_PER_DAY; }, isException: function() { return !this.isNew() && this.recurrenceId; }, isOccurrence: function() { return this.isNew() && this.recurrenceId; }, isRecurring: function() { return !!(this.recurrenceRule || this.recurrenceId); }, isRecurrenceHead: function() { return !!(this.id && this.recurrenceRule); }, toOccurrence: function(options) { options = $.extend(options, { recurrenceException: null, recurrenceRule: null, recurrenceId: this.id || this.recurrenceId }); options[this.idField] = this.defaults[this.idField]; return this.clone(options, true); }, toJSON: function() { var obj = kendo.data.Model.fn.toJSON.call(this); obj.uid = this.uid; delete obj._startTime; delete obj._endTime; return obj; }, shouldSerialize: function(field) { return kendo.data.Model.fn.shouldSerialize.call(this, field) && field !== "_defaultId"; }, set: function(key, value) { var isAllDay = this.isAllDay || false; kendo.data.Model.fn.set.call(this, key, value); if (key == "isAllDay" && value != isAllDay) { var start = kendo.date.getDate(this.start); var end = new Date(this.end); var milliseconds = kendo.date.getMilliseconds(end); if (milliseconds === 0 && value) { milliseconds = MS_PER_DAY; } this.set("start", start); if (value === true) { kendo.date.setTime(end, -milliseconds); if (end < start) { end = start; } } else { kendo.date.setTime(end, MS_PER_DAY - milliseconds); } this.set("end", end); } }, id: "id", fields: { id: { type: "number" }, title: { defaultValue: "", type: "string" }, start: { type: "date", validation: { required: true, validDate: { value: validDateValidator } } }, startTimezone: { type: "string" }, end: { type: "date", validation: { required: true, validDate: { value: validDateValidator }, dateCompare: { value: dateCompareValidator } } }, endTimezone: { type: "string" }, recurrenceRule: { defaultValue: "", type: "string", validation: { validDate: { value: validDateValidator }, untilDateCompare: { value: untilDateCompareValidator } } }, recurrenceException: { defaultValue: "", type: "string" }, isAllDay: { type: "boolean", defaultValue: false }, description: { type: "string" } } }); var SchedulerDataSource = DataSource.extend({ init: function(options) { DataSource.fn.init.call(this, extend(true, {}, { schema: { modelBase: SchedulerEvent, model: SchedulerEvent } }, options)); this.reader = new SchedulerDataReader(this.options.schema, this.reader); }, expand: function(start, end) { var data = this.view(), filter = {}, endOffset; if (start && end) { endOffset = end.getTimezoneOffset(); end = new Date(end.getTime() + MS_PER_DAY - 1); if (end.getTimezoneOffset() !== endOffset) { end = kendo.timezone.apply(end, endOffset); } filter = { logic: "or", filters: [{ logic: "and", filters: [ { field: "start", operator: "gte", value: start }, { field: "end", operator: "gte", value: start }, { field: "start", operator: "lte", value: end } ] }, { logic: "and", filters: [{ field: "start", operator: "lte", value: new Date(start.getTime() + MS_PER_DAY - 1) }, { field: "end", operator: "gte", value: start }] }] }; data = new kendo.data.Query(expandAll(data, start, end, this.reader.timezone)).filter(filter).toArray(); } return data; }, cancelChanges: function(model) { if (model && model.isOccurrence()) { this._removeExceptionDate(model); } DataSource.fn.cancelChanges.call(this, model); }, insert: function(index, model) { if (!model) { return; } if (!(model instanceof SchedulerEvent)) { var eventInfo = model; model = this._createNewModel(); model.accept(eventInfo); } if (!this._pushCreated && model.isRecurrenceHead() || model.recurrenceId) { model = model.recurrenceId ? model : model.toOccurrence(); this._addExceptionDate(model); } return DataSource.fn.insert.call(this, index, model); }, pushCreate: function(items) { this._pushCreated = true; DataSource.fn.pushCreate.call(this, items); this._pushCreated = false; }, remove: function(model) { if (model.isRecurrenceHead()) { this._removeExceptions(model); } else if (model.isRecurring()) { this._addExceptionDate(model); } return DataSource.fn.remove.call(this, model); }, _removeExceptions: function(model) { var data = this.data().slice(0), item = data.shift(), id = model.id; while (item) { if (item.recurrenceId === id) { DataSource.fn.remove.call(this, item); } item = data.shift(); } model.set(RECURRENCE_EXCEPTION, ""); }, _removeExceptionDate: function(model) { if (model.recurrenceId) { var head = this.get(model.recurrenceId); if (head) { var start = model.defaults.start; var replaceRegExp = new RegExp("(\\" + EXCEPTION_SEPARATOR + "?)" + recurrence.toExceptionString(start, this.reader.timezone)); var recurrenceException = (head.recurrenceException || "").replace(OLD_EXCEPTION_SEPARATOR_REGEXP, EXCEPTION_SEPARATOR).replace(/\,$/, ""); if (replaceRegExp.test(recurrenceException)) { head.set(RECURRENCE_EXCEPTION, recurrenceException.replace(replaceRegExp, "")); } else { start = model.start; replaceRegExp = new RegExp("(\\" + EXCEPTION_SEPARATOR + "?)" + recurrence.toExceptionString(start, this.reader.timezone)); head.set(RECURRENCE_EXCEPTION, recurrenceException.replace(replaceRegExp, "")); } } } }, _addExceptionDate: function(model) { var start = model.start; var zone = this.reader.timezone; var head = this.get(model.recurrenceId); var recurrenceException = (head.recurrenceException || "").replace(OLD_EXCEPTION_SEPARATOR_REGEXP, EXCEPTION_SEPARATOR).replace(/\,$/, ""); if (!recurrence.isException(recurrenceException, start, zone)) { var newException = recurrence.toExceptionString(start, zone); model.defaults.start = start; head.set(RECURRENCE_EXCEPTION, recurrenceException + (recurrenceException && newException ? EXCEPTION_SEPARATOR : "") + newException); } } }); function expandAll(events, start, end, zone) { var length = events.length, data = [], idx = 0; for (; idx < length; idx++) { data = data.concat(events[idx].expand(start, end, zone)); } return data; } SchedulerDataSource.create = function(options) { if (isArray(options) || options instanceof kendo.data.ObservableArray) { options = { data: options }; } var dataSource = options || {}, data = dataSource.data; dataSource.data = data; if (!(dataSource instanceof SchedulerDataSource) && dataSource instanceof kendo.data.DataSource) { throw new Error("Incorrect DataSource type. Only SchedulerDataSource instances are supported"); } return dataSource instanceof SchedulerDataSource ? dataSource : new SchedulerDataSource(dataSource); }; extend(true, kendo.data, { SchedulerDataSource, SchedulerDataReader, SchedulerEvent }); var defaultCommands = { update: { text: "Save", className: "k-button-primary k-scheduler-update" }, canceledit: { text: "Cancel", className: "k-scheduler-cancel" }, destroy: { text: "Delete", icon: "trash", imageClass: "k-i-trash", className: "k-button-primary k-scheduler-delete", iconClass: "k-icon" } }; function trimOptions(options, overrideOptions) { delete options.name; delete options.prefix; delete options.remove; delete options.edit; delete options.add; delete options.navigate; for (var key in overrideOptions) { options[key] = overrideOptions[key]; } return options; } function descriptionEditor(options) { var attr = createValidationAttributes(options.model, options.field); return function(container, model) { $("<textarea name=\"description\" class=\"k-input-inner\" title=\"" + model.title + "\"/>").attr(attr).appendTo(container).wrap("<span class=\"k-input k-textarea\"></span>"); }; } function createValidationAttributes(model, field) { var modelField = (model.fields || model)[field]; var specialRules = [ "url", "email", "number", "date", "boolean" ]; var validation = modelField ? modelField.validation : {}; var datatype = kendo.attr("type"); var inArray = $.inArray; var ruleName; var rule; var attr = {}; for (ruleName in validation) { rule = validation[ruleName]; if (inArray(ruleName, specialRules) >= 0) { attr[datatype] = ruleName; } else if (!kendo.isFunction(rule)) { attr[ruleName] = isPlainObject(rule) ? rule.value || ruleName : rule; } attr[kendo.attr(ruleName + "-msg")] = rule.message; } return attr; } function filterResourceEditorData(editor, parentValue, parentValueField, valueField) { var editorValue = editor.value(), isMs = Array.isArray(editorValue), valueArray; if (isMs) { valueArray = JSON.parse(JSON.stringify(editorValue)); } else { valueArray = [editorValue.toString()]; } editor.dataSource.data().forEach(function(item) { if (item[parentValueField] === null || item[parentValueField] === undefined || item[parentValueField] == parentValue) { item.set(DISABLED, false); } else { var currentValue = item.get(valueField); item.set(DISABLED, true); if (valueArray.indexOf(currentValue) >= 0 || valueArray.indexOf(currentValue.toString()) >= 0) { if (isMs) { valueArray.splice(valueArray.indexOf(currentValue), 1); } else { editor.value(null); editor.trigger(CHANGE); } } } }); if (isMs && valueArray.length < editorValue.length) { editor.value(valueArray); editor.trigger(CHANGE); } } function bindParentValueChangeHandler(container, currentEditor, resource, parent) { var parentElement = container.closest(".k-edit-form-container").find("[data-" + kendo.ns + "bind='value:" + parent + "']"); var parentWidget = parentElement.getKendoDropDownList(); if (parentWidget) { parentWidget.bind(CHANGE, function(ev) { var parentValue = ev.sender.value(); filterResourceEditorData(currentEditor, parentValue, resource.dataParentValueField, resource.dataValueField); }); } else { parentElement.on(CHANGE, function(ev) { var parentValue = ev.target.value; filterResourceEditorData(currentEditor, parentValue, resource.dataParentValueField, resource.dataValueField); }); } } function filterMobileResourceEditorData(resource, currentEditor, parentSelectedValue) { var options = currentEditor.find(OPTION), editorValue = currentEditor.val(), isMs = Array.isArray(editorValue), valueArray; if (isMs) { valueArray = JSON.parse(JSON.stringify(editorValue)); } else { valueArray = [editorValue]; } resource.dataSource.view().forEach(function(item, index) { var itemParentValue = kendo.getter(resource.dataParentValueField)(item); var valid = itemParentValue === null || itemParentValue === undefined || itemParentValue == parentSelectedValue; if (valid) { options[index].removeAttribute(DISABLED); } else { options[index].setAttribute(DISABLED, DISABLED); var currentValue = "" + item.get(resource.dataValueField); if (valueArray.indexOf(currentValue) >= 0) { if (isMs) { valueArray.splice(valueArray.indexOf(currentValue), 1); } else { currentEditor.val(null); currentEditor.trigger(CHANGE); } } } }); if (isMs && valueArray.length < editorValue.length) { currentEditor.val(valueArray); currentEditor.trigger(CHANGE); } } function dropDownResourceEditor(resource, model, parent) { var attr = createValidationAttributes(model, resource.field); return function(container) { var currentEditor; if (parent) { setTimeout(function() { filterResourceEditorData(currentEditor, model[parent], resource.dataParentValueField, resource.dataValueField); bindParentValueChangeHandler(container, currentEditor, resource, parent); }); } currentEditor = $(kendo.format("<select aria-labelledby=\"" + resource.field + "_label\" data-{0}bind=\"value:{1}\" title=\"" + model.title + "\">", kendo.ns, resource.field)).appendTo(container).attr(attr).kendoDropDownList({ dataTextField: resource.dataTextField, dataValueField: resource.dataValueField, dataSource: resource.dataSource.data(), valuePrimitive: resource.valuePrimitive, optionLabel: "None", template: (data) => `<span ${data.disabled ? "data-disabled" : ""}><span class="k-scheduler-mark" ${kendo.attr("style-background-color")}="${data[resource.dataColorField] || "none"}"></span>${data[resource.dataTextField]}</span>`, select: function(e) { if (e.dataItem && e.dataItem.disabled) { e.preventDefault(); } }, dataBound: function(e) { let options = e.sender.list.find("li"); for (let i = 0; i < options.length; i++) { var element = options.eq(i); if (element.find("[data-disabled]").length > 0) { element.addClass("k-disabled"); } } } }).data("kendoDropDownList"); }; } function dropDownResourceEditorMobile(resource, model, parent) { var attr = createValidationAttributes(model, resource.field); return function(container) { var options = ""; var view = resource.dataSource.view(); for (var idx = 0, length = view.length; idx < length; idx++) { options += kendo.format("<option value=\"{0}\">{1}</option>", kendo.getter(resource.dataValueField)(view[idx]), kendo.getter(resource.dataTextField)(view[idx])); } var currentEditor = $(kendo.format("<select aria-labelledby=\"" + resource.field + "_label\" data-{0}bind=\"value:{1}\">{2}</select>", kendo.ns, resource.field, options)).appendTo(container).attr(attr); if (parent) { setTimeout(function() { var parentElement = container.closest(".k-stretched-view").find("[data-" + kendo.ns + "bind='value:" + parent + "']"); var parentSelectedValue = model[parent]; filterMobileResourceEditorData(resource, currentEditor, parentSelectedValue); parentElement.on(CHANGE, function(ev) { var parentValue = ev.target.value; filterMobileResourceEditorData(resource, currentEditor, parentValue); }); }); } }; } function multiSelectResourceEditor(resource, model, parent) { var attr = createValidationAttributes(model, resource.field); return function(container) { var currentEditor; if (parent) { setTimeout(function() { filterResourceEditorData(currentEditor, model[parent], resource.dataParentValueField, resource.dataValueField); bindParentValueChangeHandler(container, currentEditor, resource, parent); }); } currentEditor = $(kendo.format("<select aria-labelledby=\"" + resource.field + "_label\" data-{0}bind=\"value:{1}\">", kendo.ns, resource.field)).appendTo(container).attr(attr).kendoMultiSelect({ dataTextField: resource.dataTextField, dataValueField: resource.dataValueField, dataSource: resource.dataSource.data(), valuePrimitive: resource.valuePrimitive, itemTemplate: (data) => `<span ${data.disabled ? "data-disabled" : ""}><span class="k-scheduler-mark" ${kendo.attr("style-background-color")}="${data[resource.dataColorField] || "none"}"></span>${data[resource.dataTextField]}</span>`, tagTemplate: (data) => `<span class="k-scheduler-mark" ${kendo.attr("style-background-color")}="${data[resource.dataColorField] || "none"}"></span>${data[resource.dataTextField]}`, select: function(e) { if (e.dataItem && e.dataItem.disabled) { e.preventDefault(); } }, dataBound: function(e) { let options = e.sender.list.find("li"); for (let i = 0; i < options.length; i++) { let element = options.eq(i); if (element.find("[data-disabled]").length > 0) { element.addClass("k-disabled"); } } } }).data("kendoMultiSelect"); }; } function multiSelectResourceEditorMobile(resource, model, parent) { var attr = createValidationAttributes(model, resource.field); return function(container) { var options = ""; var view = resource.dataSource.view(); for (var idx = 0, length = view.length; idx < length; idx++) { options += kendo.format("<option value=\"{0}\">{1}</option>", kendo.getter(resource.dataValueField)(view[idx]), kendo.getter(resource.dataTextField)(view[idx])); } var currentEditor = $(kendo.format("<select aria-labelledby=\"" + resource.field + "_label\" data-{0}bind=\"value:{1}\" multiple=\"multiple\">{2}</select>", kendo.ns, resource.field, options)).appendTo(container).attr(attr); if (parent) { setTimeout(function() { var parentElement = container.closest(".k-stretched-view").find("[data-" + kendo.ns + "bind='value:" + parent + "']"); var parentSelectedValue = model[parent]; filterMobileResourceEditorData(resource, currentEditor, parentSelectedValue); parentElement.on(CHANGE, function(ev) { var parentValue = ev.target.value; filterMobileResourceEditorData(resource, currentEditor, parentValue); }); }); } }; } function moveEventRange(event, distance) { var duration = event.end.getTime() - event.start.getTime(); var start = new Date(event.start.getTime()); kendo.date.setTime(start, distance); var end = new Date(start.getTime()); kendo.date.setTime(end, duration, true); return { start, end }; } var editors = { mobile: { dateRange: MOBILEDATERANGEEDITOR, timezonePopUp: MOBILETIMEZONEPOPUP, timezone: MOBILETIMEZONEEDITOR, recurrence: MOBILERECURRENCEEDITOR, description: descriptionEditor, multipleResources: multiSelectResourceEditorMobile, resources: dropDownResourceEditorMobile, isAllDay: MOBILEISALLDAYEDITOR }, desktop: { dateRange: DATERANGEEDITOR, timezonePopUp: TIMEZONEPOPUP, timezone: TIMEZONEEDITOR, recurrence: RECURRENCEEDITOR, description: descriptionEditor, multipleResources: multiSelectResourceEditor, resources: dropDownResourceEditor, isAllDay: ISALLDAYEDITOR } }; var Editor = kendo.Observable.extend({ init: function(element, options) { kendo.Observable.fn.init.call(this); this.element = element; this.options = extend(true, {}, this.options, options); this.createButton = this.options.createButton; this.toggleDateValidationHandler = this._toggleDateValidation.bind(this); }, _toggleDateValidation: function(e) { if (e.field == "isAllDay") { var container = this.container, isAllDay = this.editable.options.model.isAllDay, bindAttribute = kendo.attr("bind"), element, isDateTimeInput, shouldValidate; container.find("[" + bindAttribute + "*=end],[" + bindAttribute + "*=start]").each(function() { element = $(this); if (valueStartEndBoundRegex.test(element.attr(bindAttribute))) { isDateTimeInput = element.is("[" + kendo.attr("role") + "=datetimepicker],[type*=datetime]"); shouldValidate = isAllDay !== isDateTimeInput; element.attr(kendo.attr("validate"), shouldValidate.toString()); } }); } }, fields: function(editors, model) { var that = this; var messages = that.options.messages; var timezone = that.options.timezone; var click = function(e) { e.preventDefault(); that._initTimezoneEditor(model, this); }; var fields = [ { field: "title", title: messages.editor.title }, { field: "start", title: messages.editor.start, editor: editors.dateRange, timezone }, { field: "end", title: messages.editor.end, editor: editors.dateRange, timezone }, { field: "isAllDay", title: messages.editor.allDayEvent, editor: editors.isAllDay } ]; var checkHierarchical = function(item) { return !!item[resource.dataParentValueField]; }; if (kendo.timezone.windows_zones) { fields.push({ field: "timezone", title: messages.editor.timezone, editor: editors.timezonePopUp, click, messages: messages.editor, model }); fields.push({ field: "startTimezone", title: messages.editor.startTimezone, editor: editors.timezone, noTimezone: messages.editor.noTimezone }); fields.push({ field: "endTimezone", title: messages.editor.endTimezone, editor: editors.timezone, noTimezone: messages.editor.noTimezone }); } if (!model.recurrenceId) { fields.push({ field: "recurrenceRule", title: messages.editor.repeat, editor: editors.recurrence, timezone, messages: messages.recurrenceEditor, pane: this.pane }); } if ("description" in model) { fields.push({ field: "description", title: messages.editor.description, editor: editors.description({ model, field: "description" }) }); } for (var resourceIndex = 0; resourceIndex < this.options.resources.length; resourceIndex++) { var resource = this.options.resources[resourceIndex]; var resourceView = resource.dataSource.view(); var hasParent = resourceView.some(checkHierarchical); var parentResource, parent; if (hasParent) { parentResource = this.options.resources[resourceIndex - 1]; if (parentResource) { parent = parentResource.field; } } fields.push({ field: resource.field, title: resource.title, editor: resource.multiple ? editors.multipleResources(resource, model, parent) : editors.resources(resource, model, parent) }); } return fields; }, end: function() { return this.editable.end(); }, _buildDesktopEditTemplate: function(model, fields, editableFields) { var messages = this.options.messages; const startTimezone = `<div class="k-popup-edit-form k-scheduler-edit-form k-scheduler-timezones" ${kendo.attr("style-display")}="none">` + "<div class=\"k-form\">" + "<div class=\"k-form-field\"><div class=\"k-form-field-wrap\">" + kendo.html.renderCheckBox($("<input class=\"k-timezone-toggle\"/>"), { label: messages.editor.separateTimezones }) + "</div></div>"; const editableField = (fieldName) => { const isEditable = !model.editable || model.editable(fieldName); if (isEditable) { return `<div ${kendo.attr("container-for")}="${fieldName}" class="k-form-field-wrap"></div>`; } else { return `<div class="k-form-field-wrap">${fieldName && kendo.getter(fieldName)(model) || ""}</div>`; } }; const generateFields = (field) => { const fieldName = field.field; const fieldTitle = field.title; const modelField = model.fields[fieldName]; const isEditable = !model.editable || model.editable(fieldName); if (isEditable) { editableFields.push(field); } return (fieldName === "startTimezone" ? startTimezone : "") + (modelField && modelField.type === "boolean" ? `<div class="k-form-field">${editableField(fieldName)}</div>` : `<div class="k-form-field"><label class="k-label k-form-label" for="${fieldName}" id="${fieldName}_label">${fieldTitle || fieldName || ""}</label>${editableField(fieldName)}</div>`) + (fieldName === "endTimezone" ? this._createEndTimezoneButton() : ""); }; return `<div class="k-form">` + fields.map(generateFields).join("") + `</div>`; }, _buildMobileEditTemplate: function(model, fields, editableFields) { var messages = this.options.messages; const startTimezone = `<div class="k-popup-edit-form k-scheduler-edit-form k-scheduler-timezones" ${kendo.attr("style-display")}="none">` + "<ul class=\"k-listgroup k-listgroup-flush\">" + "<li class=\"k-item k-listgroup-item\">" + "<label class=\"k-label k-listgroup-form-row\">" + "<span class=\"k-item-title k-listgroup-form-row\">" + encode(messages.editor.separateTimezones) + "</span>" + "<span class=\"k-listgroup-form-field-wrapper\">" + "<input class=\"k-timezone-toggle\" data-role=\"switch\" type=\"checkbox\" />" + "</span>" + "</label>" + "</li>"; const editableField = (field) => { const fieldName = field.field; const fieldTitle = field.title; const isEditable = !model.editable || model.editable(fieldName); let midPart; if (isEditable) { midPart = `${fieldName === "timezone" ? `<label class="k-label k-listgroup-form-row" data-bind="css: { k-disabled: isAllDay }">` : `<label class="k-label k-listgroup-form-row">`}` + `<span class="k-item-title k-listgroup-form-field-label">${fieldTitle || fieldName || ""}</span>` + `<div class="k-listgroup-form-field-wrapper" ${kendo.attr("container-for")}="${fieldName}"></div>`; } else { midPart = `<li class="k-item k-listgroup-item">` + `<label class="k-label k-no-click k-listgroup-form-row">` + `<span class="k-item-title k-listgroup-form-field-label">${fieldTitle || fieldName || ""}</span>` + `<span class="k-no-editor k-listgroup-form-field-wrapper">${fieldName && kendo.getter(fieldName)(model) || ""}</span>`; } return `<li class="k-item k-listgroup-item">` + midPart + `</label></li>`; }; const generateFields = (field) => { const fieldName = field.field; const isEditable = !model.editable || model.editable(fieldName); if (isEditable) { editableFields.push(field); } return (fieldName === "timezone" || fieldName === "recurrenceRule" ? "</ul><ul class=\"k-listgroup k-listgroup-flush\">" : "") + (fieldName === "startTimezone" ? startTimezone : "") + editableField(field) + (fieldName === "recurrenceRule" ? "</ul><ul class=\"k-listgroup k-listgroup-flush\">" : "") + (fieldName === "endTimezone" ? "</ul></div>" : ""); }; const result = `<ul class="k-listgroup k-listgroup-flush">` + fields.map(generateFields).join("") + `</ul>`; return result; }, _buildEditTemplate: function(model, fields, editableFields, isMobile) { var settings = extend({}, kendo.Template, this.options.templateSettings); var template = this.options.editable.template; var html = ""; if (template) { if (typeof template === STRING) { template = kendo.unescape(template); } html += kendo.template(template, settings)(model); } else if (isMobile) { html += "<div data-role=\"content\">" + this._buildMobileEditTemplate(model, fields, editableFields) + "</div>"; } else { html += this._buildDesktopEditTemplate(model, fields, editableFields); } return html; }, _createEndTimezoneButton: function() { return "</ul></div>"; }, _revertTimezones: function(model) { model.set("startTimezone", this._startTimezone); model.set("endTimezone", this._endTimezone); delete this._startTimezone; delete this._endTimezone; } }); var MobileEditor = Editor.extend({ init: function() { Editor.fn.init.apply(this, arguments); this.pane = kendo.Pane.wrap(this.element, { viewEngine: { viewOptions: { renderOnInit: true, wrap: false, wrapInSections: true, detachOnHide: false, detachOnDestroy: false } } }); this.pane.element.parent().css("height", this.options.height); this.view = this.pane.view(); }, options: { animations: { left: "slide", right: "slide:right" } }, destroy: function() { this.close(); this.unbind(); this.pane.destroy(); }, _initTimezoneEditor: function(model) { var that = this; var pane = that.pane; var messages = that.options.messages; var timezoneView = that.timezoneView; var container = timezoneView ? timezoneView.content.find(".k-scheduler-timezones") : that.container.find(".k-scheduler-timezones"); var kSwitch = container.find("input.k-timezone-toggle").data("kendoSwitch"); var endTimezoneRow = container.find("li.k-item:not(.k-zonepicker)").last(); var startTimezoneChange = function(e) { if (e.field === "startTimezone") { var value = model.startTimezone; kSwitch.enable(value); if (!value) { endTimezoneRow.hide(); model.set("endTimezone", ""); kSwitch.value(false); } } }; that._startTimezone = model.startTimezone || ""; that._endTimezone = model.endTimezone || ""; if (!timezoneView) { var html = "<div data-role=\"view\" class=\"k-popup-edit-form k-scheduler-edit-form\">" + "<div data-role=\"header\" class=\"k-appbar k-appbar-primary\">" + kendo.html.renderButton(`<button class="k-header-cancel k-scheduler-cancel" title="${messages.cancel}" aria-label="${messages.cancel}"></button>`, { fillMode: "flat", icon: "chevron-left" }) + "<span class=\"k-spacer\"></span>" + `<span>${encode(messages.editor.timezoneTitle)}</span>` + "<span class=\"k-spacer\"></span>" + encode(messages.editor.timezoneTitle) + kendo.html.renderButton(`<button class="k-header-done k-scheduler-update" title="${messages.save}" aria-label="${messages.save}"></button>`, { fillMode: "flat", icon: "check" }) + "</div><div data-role=\"content\"></div>"; this.timezoneView = timezoneView = pane.append(html); timezoneView.contentElement.append(container.show()); timezoneView.element.on(CLICK + NS, ".k-scheduler-cancel, .k-scheduler-update", function(e) { e.preventDefault(); e.stopPropagation(); if ($(this).hasClass("k-sched