UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

721 lines (720 loc) • 28.9 kB
/** * DevExtreme (esm/__internal/scheduler/m_recurrence_editor.js) * Version: 25.2.7 * Build date: Tue May 05 2026 * * Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import "../../ui/radio_group"; import dateLocalization from "../../common/core/localization/date"; import messageLocalization from "../../common/core/localization/message"; import registerComponent from "../../core/component_registrator"; import $ from "../../core/renderer"; import dateUtils from "../../core/utils/date"; import { extend } from "../../core/utils/extend"; import { isDefined } from "../../core/utils/type"; import ButtonGroup from "../../ui/button_group"; import Editor from "../../ui/editor/editor"; import Form from "../../ui/form"; import { current, isFluent } from "../../ui/themes"; import { getRecurrenceString, parseRecurrenceRule } from "./recurrence/base"; import { daysFromByDayRule } from "./recurrence/days_from_by_day_rule"; const RECURRENCE_EDITOR = "dx-recurrence-editor"; const LABEL_POSTFIX = "-label"; const WRAPPER_POSTFIX = "-wrapper"; const RECURRENCE_EDITOR_CONTAINER = "dx-recurrence-editor-container"; const REPEAT_END_TYPE_EDITOR = "dx-recurrence-radiogroup-repeat-type"; const REPEAT_COUNT_EDITOR = "dx-recurrence-numberbox-repeat-count"; const REPEAT_UNTIL_DATE_EDITOR = "dx-recurrence-datebox-until-date"; const RECURRENCE_BUTTON_GROUP = "dx-recurrence-button-group"; const FREQUENCY_EDITOR = "dx-recurrence-selectbox-freq"; const INTERVAL_EDITOR = "dx-recurrence-numberbox-interval"; const REPEAT_ON_EDITOR = "dx-recurrence-repeat-on"; const DAY_OF_MONTH = "dx-recurrence-numberbox-day-of-month"; const MONTH_OF_YEAR = "dx-recurrence-selectbox-month-of-year"; const recurrentEditorNumberBoxWidth = 90; const repeatInputWidth = "100%"; const recurrentEditorSelectBoxWidth = 120; const defaultRecurrenceTypeIndex = 1; const frequenciesMessages = [{ recurrence: "dxScheduler-recurrenceHourly", value: "hourly" }, { recurrence: "dxScheduler-recurrenceDaily", value: "daily" }, { recurrence: "dxScheduler-recurrenceWeekly", value: "weekly" }, { recurrence: "dxScheduler-recurrenceMonthly", value: "monthly" }, { recurrence: "dxScheduler-recurrenceYearly", value: "yearly" }]; const frequencies = frequenciesMessages.map(item => ({ text: () => messageLocalization.format(item.recurrence), value: item.value })); const repeatEndTypes = [{ type: "never" }, { type: "until" }, { type: "count" }]; const days = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"]; const getStylingModeFunc = () => isFluent(current()) ? "filled" : void 0; class RecurrenceRule { constructor(rule) { this.recurrenceRule = parseRecurrenceRule(rule) } makeRules(string) { this.recurrenceRule = parseRecurrenceRule(string) } makeRule(field, value) { if (!value || Array.isArray(value) && !value.length) { delete this.recurrenceRule[field]; return } if (isDefined(field)) { if ("until" === field) { delete this.recurrenceRule.count } if ("count" === field) { delete this.recurrenceRule.until } this.recurrenceRule[field] = value } } getRepeatEndRule() { const rules = this.recurrenceRule; if ("count" in rules) { return "count" } if ("until" in rules) { return "until" } return "never" } getRecurrenceString() { return getRecurrenceString(this.recurrenceRule) } getRules() { return this.recurrenceRule } getDaysFromByDayRule() { return daysFromByDayRule(this.recurrenceRule) } } class RecurrenceEditor extends Editor { _getDefaultOptions() { const defaultOptions = super._getDefaultOptions(); return extend(defaultOptions, { value: null, startDate: new Date, firstDayOfWeek: void 0 }) } getFirstDayOfWeek() { const firstDayOfWeek = this.option("firstDayOfWeek"); return isDefined(firstDayOfWeek) ? firstDayOfWeek : dateLocalization.firstDayOfWeekIndex() } createComponent(element, name) { let config = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; this._extendConfig(config, { readOnly: this.option("readOnly") }); return super._createComponent(element, name, config) } _init() { super._init(); this.recurrenceRule = new RecurrenceRule(this.option("value")) } _render() { super._render(); this.$element().addClass(RECURRENCE_EDITOR); this.$container = $("<div>").addClass(RECURRENCE_EDITOR_CONTAINER).appendTo(this.$element()); this.prepareEditors(); this.renderEditors(this.$container); this.updateRepeatInputAriaLabel() } getEditorByField(fieldName) { let editor = this.getRecurrenceForm().getEditor(fieldName); if (!isDefined(editor)) { if ("byday" === fieldName) { editor = this.weekEditor } } return editor } prepareEditors() { const freq = (this.recurrenceRule.getRules().freq || frequenciesMessages[1].value).toLowerCase(); this.editors = [this.createFreqEditor(freq), this.createIntervalEditor(freq), this.createRepeatOnLabel(freq), { itemType: "group", cssClass: REPEAT_ON_EDITOR, colCount: 2, colCountByScreen: { xs: 2 }, items: this.createRepeatOnEditor(freq) }, { itemType: "group", colCount: 2, items: this.createRepeatEndEditor() }]; return this.editors } createFreqEditor(freq) { return { dataField: "freq", name: "FREQ", editorType: "dxSelectBox", cssClass: FREQUENCY_EDITOR, editorOptions: { stylingMode: getStylingModeFunc(), items: frequencies, value: freq, field: "freq", valueExpr: "value", displayExpr: "text", layout: "horizontal", elementAttr: { class: FREQUENCY_EDITOR }, onValueChanged: args => this.valueChangedHandler(args) }, label: { text: messageLocalization.format("dxScheduler-editorLabelRecurrence") } } } createIntervalEditor(freq) { const interval = this.recurrenceRule.getRules().interval || 1; return { itemType: "group", colCount: 2, cssClass: `${INTERVAL_EDITOR}-wrapper`, colCountByScreen: { xs: 2 }, items: [{ dataField: "interval", editorType: "dxNumberBox", editorOptions: { stylingMode: getStylingModeFunc(), format: "#", width: 90, min: 1, field: "interval", value: interval, showSpinButtons: true, useLargeSpinButtons: false, elementAttr: { class: INTERVAL_EDITOR }, onValueChanged: args => this.valueChangedHandler(args) }, label: { text: messageLocalization.format("dxScheduler-recurrenceRepeatEvery") } }, { name: "intervalLabel", cssClass: `${INTERVAL_EDITOR}-label`, template: () => messageLocalization.format(`dxScheduler-recurrenceRepeat${freq.charAt(0).toUpperCase()}${freq.substr(1).toLowerCase()}`) }] } } createRepeatOnLabel(freq) { return { itemType: "group", cssClass: `${REPEAT_ON_EDITOR}-label`, items: [{ name: "repeatOnLabel", colSpan: 2, template: () => messageLocalization.format("dxScheduler-recurrenceRepeatOn"), visible: freq && "daily" !== freq && "hourly" !== freq }] } } createRepeatOnEditor(freq) { return [this.createByDayEditor(freq), this.createByMonthEditor(freq), this.createByMonthDayEditor(freq)] } createByDayEditor(freq) { return { dataField: "byday", colSpan: 2, template: (_, itemElement) => { const firstDayOfWeek = this.getFirstDayOfWeek(); const byDay = this.daysOfWeekByRules(); const localDaysNames = dateLocalization.getDayNames("abbreviated"); const dayNames = days.slice(firstDayOfWeek).concat(days.slice(0, firstDayOfWeek)); const itemsButtonGroup = localDaysNames.slice(firstDayOfWeek).concat(localDaysNames.slice(0, firstDayOfWeek)).map((item, index) => ({ text: item, key: dayNames[index] })); this.$repeatOnWeek = $("<div>").addClass(RECURRENCE_BUTTON_GROUP).appendTo(itemElement); this.weekEditor = this.createComponent(this.$repeatOnWeek, ButtonGroup, { items: itemsButtonGroup, field: "byday", selectionMode: "multiple", selectedItemKeys: byDay, keyExpr: "key", onSelectionChanged: e => { const selectedItemKeys = e.component.option("selectedItemKeys"); const selectedKeys = null !== selectedItemKeys && void 0 !== selectedItemKeys && selectedItemKeys.length ? selectedItemKeys : this.getDefaultByDayValue(); this.recurrenceRule.makeRule("byday", selectedKeys); this.changeEditorValue() } }) }, visible: "weekly" === freq, label: { visible: false } } } createByMonthEditor(freq) { const monthsName = dateLocalization.getMonthNames("wide"); const months = [...Array(12)].map((_, i) => ({ value: `${i+1}`, text: monthsName[i] })); return { dataField: "bymonth", editorType: "dxSelectBox", editorOptions: { stylingMode: getStylingModeFunc(), field: "bymonth", items: months, value: this.monthOfYearByRules(), width: 120, displayExpr: "text", valueExpr: "value", elementAttr: { class: MONTH_OF_YEAR }, onValueChanged: args => this.valueChangedHandler(args) }, visible: "yearly" === freq, label: { visible: false } } } createByMonthDayEditor(freq) { return { dataField: "bymonthday", editorType: "dxNumberBox", editorOptions: { stylingMode: getStylingModeFunc(), min: 1, max: 31, format: "#", width: 90, field: "bymonthday", showSpinButtons: true, useLargeSpinButtons: false, value: this.dayOfMonthByRules(), elementAttr: { class: DAY_OF_MONTH }, onValueChanged: args => this.valueChangedHandler(args) }, visible: "monthly" === freq || "yearly" === freq, label: { visible: false } } } createRepeatEndEditor() { const repeatType = this.recurrenceRule.getRepeatEndRule(); return [{ colSpan: 2, template: messageLocalization.format("dxScheduler-recurrenceEnd") }, { colSpan: 1, label: { visible: false }, dataField: "repeatEnd", editorType: "dxRadioGroup", editorOptions: { items: repeatEndTypes, value: repeatType, valueExpr: "type", field: "repeatEnd", itemTemplate: itemData => { if ("count" === itemData.type) { return messageLocalization.format("dxScheduler-recurrenceAfter") } if ("until" === itemData.type) { return messageLocalization.format("dxScheduler-recurrenceOn") } return messageLocalization.format("dxScheduler-recurrenceNever") }, layout: "vertical", elementAttr: { class: REPEAT_END_TYPE_EDITOR }, onValueChanged: args => this.repeatEndValueChangedHandler(args) } }, { colSpan: 1, itemType: "group", items: [this.getRepeatUntilEditorOptions(), this.getRepeatCountEditorOptions()] }] } renderEditors($container) { this.recurrenceForm = this.createComponent($container, Form, { items: this.editors, showValidationSummary: false, scrollingEnabled: true, showColonAfterLabel: false, labelLocation: "top" }); this.changeRepeatEndInputsVisibility() } getRecurrenceForm() { return this.recurrenceForm } changeValueByVisibility(value) { if (value) { if (!this.option("value")) { this.handleDefaults() } } else { this.recurrenceRule.makeRules(""); this.option("value", "") } } handleDefaults() { this.recurrenceRule.makeRule("freq", frequenciesMessages[1].value); this.changeEditorValue() } changeEditorValue() { this.option("value", this.recurrenceRule.getRecurrenceString() ?? "") } daysOfWeekByRules() { let daysByRule = this.recurrenceRule.getDaysFromByDayRule(); if (!daysByRule.length) { daysByRule = this.getDefaultByDayValue() } return daysByRule } getDefaultByDayValue() { const startDate = this.option("startDate"); const startDay = startDate.getDay(); return [days[startDay]] } dayOfMonthByRules() { let dayByRule = this.recurrenceRule.getRules().bymonthday; if (!dayByRule) { dayByRule = this.option("startDate").getDate() } return dayByRule } monthOfYearByRules() { let monthByRule = this.recurrenceRule.getRules().bymonth; if (!monthByRule) { monthByRule = this.option("startDate").getMonth() + 1 } return String(monthByRule) } repeatEndValueChangedHandler(args) { const { value: value } = args; this.changeRepeatEndInputsVisibility(value); if ("until" === value) { this.recurrenceRule.makeRule(value, this.getUntilValue()) } if ("count" === value) { this.recurrenceRule.makeRule(value, this.recurrenceForm.option("formData.count")) } if ("never" === value) { this.recurrenceRule.makeRule("count", ""); this.recurrenceRule.makeRule("until", "") } this.changeEditorValue(); this.updateRepeatInputAriaLabel() } changeRepeatEndInputsVisibility() { let value = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : this.recurrenceRule.getRepeatEndRule(); if ("until" === value) { this.recurrenceForm.itemOption("until", "visible", true); this.recurrenceForm.itemOption("count", "visible", false) } if ("count" === value) { this.recurrenceForm.itemOption("until", "visible", false); this.recurrenceForm.itemOption("count", "visible", true) } if ("never" === value) { this.recurrenceForm.itemOption("until", "visible", false); this.recurrenceForm.itemOption("count", "visible", false) } } getRepeatCountEditorOptions() { const count = this.recurrenceRule.getRules().count || 1; return { dataField: "count", cssClass: REPEAT_COUNT_EDITOR, label: { visible: false }, editorType: "dxNumberBox", editorOptions: { stylingMode: getStylingModeFunc(), field: "count", format: `# ${messageLocalization.format("dxScheduler-recurrenceRepeatCount")}`, width: "100%", min: 1, showSpinButtons: true, useLargeSpinButtons: false, value: count, onValueChanged: this.repeatCountValueChangeHandler.bind(this), inputAttr: { "aria-label": messageLocalization.format("dxScheduler-recurrenceOccurrenceLabel") } } } } updateRepeatInputAriaLabel() { const radioButtons = this.getEditorByField("repeatEnd").itemElements(); const untilLabel = messageLocalization.format("dxScheduler-recurrenceOn"); const untilValue = this.recurrenceForm.getEditor("until").option("value"); const untilValueFormat = `${dateLocalization.format(untilValue,"d")} ${dateLocalization.format(untilValue,"monthAndYear")}`; const isUntilVisible = this.recurrenceForm.itemOption("until").visible; const countLabel = messageLocalization.format("dxScheduler-recurrenceAfter"); const countPostfix = messageLocalization.format("dxScheduler-recurrenceRepeatCount"); const countValue = this.recurrenceForm.getEditor("count").option("value"); const isCountVisible = this.recurrenceForm.itemOption("count").visible; radioButtons[1].setAttribute("aria-label", isUntilVisible ? `${untilLabel} ${untilValueFormat}` : untilLabel); radioButtons[2].setAttribute("aria-label", isCountVisible ? `${countLabel} ${countValue} ${countPostfix}` : countLabel) } repeatCountValueChangeHandler(args) { if ("count" === this.recurrenceRule.getRepeatEndRule()) { const { value: value } = args; this.recurrenceRule.makeRule("count", value); this.changeEditorValue(); this.updateRepeatInputAriaLabel() } } getRepeatUntilEditorOptions() { const until = this.getUntilValue(); return { dataField: "until", label: { visible: false }, cssClass: REPEAT_UNTIL_DATE_EDITOR, editorType: "dxDateBox", editorOptions: { stylingMode: getStylingModeFunc(), field: "until", value: until, type: "date", width: "100%", onValueChanged: this.repeatUntilValueChangeHandler.bind(this), calendarOptions: { firstDayOfWeek: this.getFirstDayOfWeek() }, useMaskBehavior: true, inputAttr: { "aria-label": messageLocalization.format("dxScheduler-recurrenceUntilDateLabel") } } } } formatUntilDate(date) { const untilDate = this.recurrenceRule.getRules().until; const isSameDate = dateUtils.sameDate(untilDate, date); return untilDate && isSameDate ? date : dateUtils.setToDayEnd(date) } repeatUntilValueChangeHandler(args) { if ("until" === this.recurrenceRule.getRepeatEndRule()) { const dateInTimeZone = this.formatUntilDate(new Date(args.value)); const getStartDateTimeZone = this.option("getStartDateTimeZone"); const appointmentTimeZone = getStartDateTimeZone(); const path = appointmentTimeZone ? "fromAppointment" : "fromGrid"; const dateInLocaleTimeZone = this.option("timeZoneCalculator").createDate(dateInTimeZone, path, appointmentTimeZone); this.recurrenceRule.makeRule("until", dateInLocaleTimeZone); this.changeEditorValue(); this.updateRepeatInputAriaLabel() } } valueChangedHandler(args) { const { value: value, previousValue: previousValue } = args; const field = args.component.option("field"); if (!this.option("visible")) { this.option("value", "") } else { this.recurrenceRule.makeRule(field, value); if ("freq" === field) { this.makeRepeatOnRule(value); this.changeRepeatOnVisibility(value, previousValue) } this.changeEditorValue() } } makeRepeatOnRule(value) { if ("daily" === value || "hourly" === value) { this.recurrenceRule.makeRule("byday", ""); this.recurrenceRule.makeRule("bymonth", ""); this.recurrenceRule.makeRule("bymonthday", "") } if ("weekly" === value) { this.recurrenceRule.makeRule("byday", this.daysOfWeekByRules()); this.recurrenceRule.makeRule("bymonth", ""); this.recurrenceRule.makeRule("bymonthday", "") } if ("monthly" === value) { this.recurrenceRule.makeRule("bymonthday", this.dayOfMonthByRules()); this.recurrenceRule.makeRule("bymonth", ""); this.recurrenceRule.makeRule("byday", "") } if ("yearly" === value) { this.recurrenceRule.makeRule("bymonthday", this.dayOfMonthByRules()); this.recurrenceRule.makeRule("bymonth", this.monthOfYearByRules()); this.recurrenceRule.makeRule("byday", "") } } _optionChanged(args) { var _this$recurrenceForm, _this$weekEditor; switch (args.name) { case "readOnly": null === (_this$recurrenceForm = this.recurrenceForm) || void 0 === _this$recurrenceForm || _this$recurrenceForm.option("readOnly", args.value); null === (_this$weekEditor = this.weekEditor) || void 0 === _this$weekEditor || _this$weekEditor.option("readOnly", args.value); super._optionChanged(args); break; case "value": this.recurrenceRule.makeRules(args.value); this.changeRepeatIntervalLabel(); this.changeRepeatEndInputsVisibility(); this.changeEditorsValue(this.recurrenceRule.getRules()); super._optionChanged(args); break; case "startDate": this.makeRepeatOnRule(this.recurrenceRule.getRules().freq); if (isDefined(this.recurrenceRule.getRecurrenceString())) { this.changeEditorValue() } break; case "firstDayOfWeek": if (this.weekEditor) { const localDaysNames = dateLocalization.getDayNames("abbreviated"); const dayNames = days.slice(args.value).concat(days.slice(0, args.value)); const itemsButtonGroup = localDaysNames.slice(args.value).concat(localDaysNames.slice(0, args.value)).map((item, index) => ({ text: item, key: dayNames[index] })); this.weekEditor.option("items", itemsButtonGroup) } if (this.recurrenceForm.itemOption("until").visible) { this.recurrenceForm.getEditor("until").option("calendarOptions.firstDayOfWeek", this.getFirstDayOfWeek()) } break; default: super._optionChanged(args) } } changeRepeatOnVisibility(freq, previousFreq) { if (freq !== previousFreq) { this.recurrenceForm.itemOption("byday", "visible", false); this.recurrenceForm.itemOption("bymonthday", "visible", false); this.recurrenceForm.itemOption("bymonth", "visible", false); this.recurrenceForm.itemOption("repeatOnLabel", "visible", freq && "daily" !== freq && "hourly" !== freq); if ("weekly" === freq) { this.recurrenceForm.itemOption("byday", "visible", true) } if ("monthly" === freq) { this.recurrenceForm.itemOption("bymonthday", "visible", true) } if ("yearly" === freq) { this.recurrenceForm.itemOption("bymonthday", "visible", true); this.recurrenceForm.itemOption("bymonth", "visible", true) } } } changeRepeatIntervalLabel() { const { freq: freq } = this.recurrenceRule.getRules(); freq && this.recurrenceForm.itemOption("intervalLabel", "template", messageLocalization.format(`dxScheduler-recurrenceRepeat${freq.charAt(0).toUpperCase()}${freq.substr(1).toLowerCase()}`)) } changeEditorsValue(rules) { this.recurrenceForm.getEditor("freq").option("value", (rules.freq || frequenciesMessages[1].value).toLowerCase()); this.changeDayOfWeekValue(); this.changeDayOfMonthValue(); this.changeMonthOfYearValue(); this.changeIntervalValue(rules.interval); this.changeRepeatCountValue(); this.changeRepeatEndValue(); this.changeRepeatUntilValue() } changeIntervalValue(value) { this.recurrenceForm.getEditor("interval").option("value", value || 1) } changeRepeatEndValue() { const repeatType = this.recurrenceRule.getRepeatEndRule(); this.recurrenceForm.getEditor("repeatEnd").option("value", repeatType) } changeDayOfWeekValue() { const isEditorVisible = this.recurrenceForm.itemOption("byday").visible; if (isEditorVisible) { const days = this.daysOfWeekByRules(); this.getEditorByField("byday").option("selectedItemKeys", days) } } changeDayOfMonthValue() { const isEditorVisible = this.recurrenceForm.itemOption("bymonthday").visible; if (isEditorVisible) { const day = this.dayOfMonthByRules(); this.recurrenceForm.getEditor("bymonthday").option("value", day) } } changeMonthOfYearValue() { const isEditorVisible = this.recurrenceForm.itemOption("bymonth").visible; if (isEditorVisible) { const month = this.monthOfYearByRules(); this.recurrenceForm.getEditor("bymonth").option("value", month) } } changeRepeatCountValue() { const count = this.recurrenceRule.getRules().count || 1; this.recurrenceForm.getEditor("count").option("value", count) } changeRepeatUntilValue() { this.recurrenceForm.getEditor("until").option("value", this.getUntilValue()) } getUntilValue() { const untilDate = this.recurrenceRule.getRules().until; if (!untilDate) { return this.formatUntilDate(new Date) } const getStartDateTimeZone = this.option("getStartDateTimeZone"); const appointmentTimeZone = getStartDateTimeZone(); const path = appointmentTimeZone ? "toAppointment" : "toGrid"; return this.option("timeZoneCalculator").createDate(untilDate, path, appointmentTimeZone) } } registerComponent("dxRecurrenceEditor", RecurrenceEditor); export default RecurrenceEditor;