---
title: "Javascript Event calendar Recurring Events Editor Example | Mobiscroll"
component: "Event calendar"
framework: "JavaScript"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/javascript/eventcalendar/recurring-event-add-edit-dialog"
---

# Recurring Events Editor - JavaScript Event calendar demo

Add recurring event management to a Javascript event calendar — use preset daily, weekly, monthly, and yearly rules or a custom recurrence editor. Plain JavaScript API.

- [Live demo](https://demo.mobiscroll.com/javascript/eventcalendar/recurring-event-add-edit-dialog)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-javascript/tree/main/src/demos/eventcalendar/calendar-view/recurring-event-add-edit-dialog)
- This demo in other frameworks: [jQuery](https://demo.mobiscroll.com/jquery/eventcalendar/recurring-event-add-edit-dialog.md), [Angular](https://demo.mobiscroll.com/angular/eventcalendar/recurring-event-add-edit-dialog.md), [React](https://demo.mobiscroll.com/react/eventcalendar/recurring-event-add-edit-dialog.md), [Vue](https://demo.mobiscroll.com/vue/eventcalendar/recurring-event-add-edit-dialog.md)

## Description

As seen in the [previous example](https://demo.mobiscroll.com/javascript/eventcalendar/create-read-update-delete-CRUD), an add/edit form can be created with custom and base event fields. While the CRUD example features delete with undo, this example shows off how to build a fully customizable recurrence editor right in the add/edit screen.

Above some common presets, like **Daily**, **Weekly on X day**, **Monthly on Xth**, **Annually on the same day**, **Every Weekday** a fully custom recurrence editor is implemented.

Copy & paste or delve into the code and understand how to generate recurrence objects or RRULES as seen in this [live rule builder](https://demo.mobiscroll.com/javascript/eventcalendar/recurring-events).

## What this demo shows

- A desktop month view event calendar with recurring events already rendered across the month.
- **Calendar header** The header shows the current month and year on the left, and blue month navigation arrows with a `Today` button (for jumping back to the current date) between them on the right. 
- **Event creation** Users can create a new event by clicking and dragging across calendar cells or by double-clicking a day cell.
- **Create dialog** Creating an event opens an event editor with fields for title, description, all-day, start date, end date, and recurrence.
- **Date selection** The start and end fields open a date picker with range selection.
- **Recurring presets** The Repeats field opens a dropdown with preset options including Does not repeat, Daily, Weekly, Monthly, Yearly, and Custom.
- **Custom recurrence** Choosing Custom expands the editor with additional controls for defining a custom recurring rule.
- **Event interaction** Hovering over an event label highlights it. This action shows resize handles on both sides, indicating that the event can be resized by clicking and dragging.
- **Event editing** Clicking an existing event opens the same dialog in edit mode with the current event values prefilled.
- **Save and cancel flow** Clicking outside the dialog or pressing Cancel closes the editor without applying changes, while Save applies the changes to the event.
- **Delete flow** In edit mode, the dialog includes a Delete event action for removing the selected event.
- **Recurring delete options** When deleting a recurring event, a confirmation popup appears with options for This event only, This and following events, and All events, plus Cancel and Ok actions.
- **Responsive editor** The event editor adapts to screen size and device type, using touch-friendly controls such as scrollers on smaller or touch devices.
- **Cell hover and selection** Hovering a day cell highlights the day number with a gray background, while clicking an empty part of a day cell selects that day and highlights the day number with a blue background.

## Best for

- **Recurring event management** Apps that need users to create, edit, and delete repeating events directly from a calendar view.
- **Exception handling** Scenarios where users may need to delete a single occurrence, all following occurrences, or the entire recurring series.
- **Business and operational scheduling** Internal tools or customer-facing products for planning appointments, classes, shifts, programs, or other repeating activities.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-themeVariant) - option
- [`setOptions`](https://mobiscroll.com/docs/javascript/datepicker/api#method-setOptions) - method
- [`controls`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-controls) - option
- [`responsive`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-responsive) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-touchUi) - option
- [`navigate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-navigate) - method
- [`isVisible`](https://mobiscroll.com/docs/javascript/popup/api#method-isVisible) - method
- [`addEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-addEvent) - method
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`setOptions`](https://mobiscroll.com/docs/javascript/select/api#method-setOptions) - method
- [`data`](https://mobiscroll.com/docs/javascript/select/api#opt-data) - option
- [`setVal`](https://mobiscroll.com/docs/javascript/select/api#method-setVal) - method
- [`setOptions`](https://mobiscroll.com/docs/javascript/popup/api#method-setOptions) - method
- [`anchor`](https://mobiscroll.com/docs/javascript/popup/api#opt-anchor) - option
- [`headerText`](https://mobiscroll.com/docs/javascript/popup/api#opt-headerText) - option
- [`buttons`](https://mobiscroll.com/docs/javascript/popup/api#opt-buttons) - option
- [`close`](https://mobiscroll.com/docs/javascript/popup/api#method-close) - method
- [`cssClass`](https://mobiscroll.com/docs/javascript/popup/api#opt-cssClass) - option
- [`onClose`](https://mobiscroll.com/docs/javascript/popup/api#event-onClose) - event
- [`open`](https://mobiscroll.com/docs/javascript/popup/api#method-open) - method
- [`clickToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToResize) - option
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`labels`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-labels) - option
- [`data`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) - option
- [`onEventClick`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventClick) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdate) - event
- [`onEventCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreate) - event
- [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated) - event
- [`select`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-select) - option
- [`startInput`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-startInput) - option
- [`endInput`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-endInput) - option
- [`showRangeLabels`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-showRangeLabels) - option
- [`onChange`](https://mobiscroll.com/docs/javascript/datepicker/api#event-onChange) - event
- [`touchUi`](https://mobiscroll.com/docs/javascript/select/api#opt-touchUi) - option
- [`responsive`](https://mobiscroll.com/docs/javascript/select/api#opt-responsive) - option
- [`onChange`](https://mobiscroll.com/docs/javascript/select/api#event-onChange) - event
- [`maxWidth`](https://mobiscroll.com/docs/javascript/select/api#opt-maxWidth) - option
- [`display`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-display) - option
- [`dateFormat`](https://mobiscroll.com/docs/javascript/datepicker/api#localization-dateFormat) - option
- [`returnFormat`](https://mobiscroll.com/docs/javascript/datepicker/api#opt-returnFormat) - option
- [`onOpen`](https://mobiscroll.com/docs/javascript/datepicker/api#event-onOpen) - event
- [`display`](https://mobiscroll.com/docs/javascript/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/javascript/popup/api#opt-fullScreen) - option
- [`scrollLock`](https://mobiscroll.com/docs/javascript/popup/api#opt-scrollLock) - option
- [`height`](https://mobiscroll.com/docs/javascript/popup/api#opt-height) - option
- [`responsive`](https://mobiscroll.com/docs/javascript/popup/api#opt-responsive) - option
- [`width`](https://mobiscroll.com/docs/javascript/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/popup/api#opt-touchUi) - option

## Code

### JS

```js
mobiscroll.setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

function getMonthDays(month) {
  var values = [];
  for (var i = 1; i <= MAX_MONTH_DAYS[month - 1]; i++) {
    values.push(i);
  }
  return values;
}

function getRecurrenceText() {
  var text;

  switch (recurrenceRepeat) {
    case 'daily':
      text = recurrenceInterval > 1 ? 'Every ' + recurrenceInterval + ' days' : 'Daily';
      break;
    case 'weekly':
      var weekDays = recurrenceWeekDays.split(',');
      var weekDaysText = weekDays
        .map(function (weekDay) {
          return DAY_NAMES[DAY_NAMES_MAP[weekDay]];
        })
        .join(', ');
      text = recurrenceInterval > 1 ? 'Every ' + recurrenceInterval + ' weeks' : 'Weekly';
      text += ' on ' + weekDaysText;
      break;
    case 'monthly':
      text = recurrenceInterval > 1 ? 'Every ' + recurrenceInterval + ' months' : 'Monthly';
      text += ' on day ' + recurrenceDay;
      break;
    case 'yearly':
      text = recurrenceInterval > 1 ? 'Every ' + recurrenceInterval + ' years' : 'Annualy';
      text += ' on ' + MONTH_NAMES[recurrenceMonth - 1].text + ' ' + recurrenceDay;
      break;
  }

  switch (recurrenceCondition) {
    case 'until':
      text += ' until ' + mobiscroll.formatDate('MMMM D, YYYY', new Date(recurrenceUntil));
      break;
    case 'count':
      text += ', ' + recurrenceCount + ' times';
      break;
  }

  return text;
}

function getRecurrenceRule() {
  var d = new Date(eventStart);
  var weekNr = Math.ceil(d.getDate() / 7);
  var weekDay = DAY_NAMES_SHORT[d.getDay()];
  var month = d.getMonth() + 1;
  var monthDay = d.getDate();

  switch (eventRecurrence) {
    // Predefined recurring rules
    case 'daily':
      return { repeat: 'daily' };
    case 'weekly':
      return { repeat: 'weekly', weekDays: weekDay };
    case 'monthly':
      return { repeat: 'monthly', day: monthDay };
    case 'monthly-pos':
      return { repeat: 'monthly', weekDays: weekDay, pos: weekNr };
    case 'yearly':
      return { repeat: 'yearly', day: monthDay, month: month };
    case 'yearly-pos':
      return { repeat: 'yearly', weekDays: weekDay, month: month, pos: weekNr };
    case 'weekday':
      return { repeat: 'weekly', weekDays: 'MO,TU,WE,TH,FR' };
    // Custom recurring rule
    case 'custom':
    case 'custom-value':
      var rule = {
        repeat: recurrenceRepeat,
        interval: recurrenceInterval,
      };
      switch (recurrenceRepeat) {
        case 'weekly':
          rule.weekDays = recurrenceWeekDays;
          break;
        case 'monthly':
          rule.day = recurrenceDay;
          break;
        case 'yearly':
          rule.day = recurrenceDay;
          rule.month = recurrenceMonth;
          break;
      }
      switch (recurrenceCondition) {
        case 'until':
          rule.until = recurrenceUntil;
          break;
        case 'count':
          rule.count = recurrenceCount;
          break;
      }
      return rule;
    default:
      return null;
  }
}

function getRecurrenceTypes(date, recurrence) {
  var d = new Date(date);
  var weekDay = DAY_NAMES[d.getDay()];
  var weekNr = Math.ceil(d.getDate() / 7);
  var month = MONTH_NAMES[d.getMonth()].text;
  var monthDay = d.getDate();
  var ordinal = { 1: 'first', 2: 'second', 3: 'third', 4: 'fourth', 5: 'fifth' };
  var data = [
    { value: 'norepeat', text: 'Does not repeat' },
    { value: 'daily', text: 'Daily' },
    { value: 'weekly', text: 'Weekly on ' + weekDay },
    { value: 'monthly', text: 'Monthly on day ' + monthDay },
    { value: 'monthly-pos', text: 'Monthly on the ' + ordinal[weekNr] + ' ' + weekDay },
    { value: 'yearly', text: 'Annually on ' + month + ' ' + monthDay },
    { value: 'yearly-pos', text: 'Annually on the ' + ordinal[weekNr] + ' ' + weekDay + ' of ' + month },
    { value: 'weekday', text: 'Every weekday (Monday to Friday)' },
    { value: 'custom', text: 'Custom' },
  ];
  if (recurrence === 'custom-value') {
    data.push({ value: 'custom-value', text: getRecurrenceText() });
  }
  return data;
}

function getEventRecurrence(event) {
  var recurringRule = event.recurring;
  var recurrenceType = 'norepeat';
  if (recurringRule) {
    var repeat = recurringRule.repeat;
    if (recurringRule.interval > 1 || recurringRule.count || recurringRule.until) {
      return 'custom-value';
    }
    recurrenceType = repeat;
    switch (repeat) {
      case 'weekly':
        var weekDays = recurringRule.weekDays || '';
        if (weekDays === 'MO,TU,WE,TH,FR') {
          recurrenceType = 'weekday';
        } else if (weekDays.split(',').length > 1) {
          recurrenceType = 'custom-value';
        }
        break;
      case 'monthly':
      case 'yearly':
        if (recurringRule.pos) {
          recurrenceType = repeat + '-pos';
        }
        break;
    }
  }
  return recurrenceType;
}

function toggleDatetimePicker(allDay) {
  // Toggle between date and datetime picker
  eventStartEndPicker.setOptions({
    controls: allDay ? ['date'] : ['datetime'],
    responsive: allDay
      ? { medium: { controls: ['calendar'], touchUi: false } }
      : { medium: { controls: ['calendar', 'time'], touchUi: false } },
  });
}

function toggleRecurrenceEditor(recurrence) {
  if (recurrence === 'custom') {
    eventRecurrenceEditorElm.classList.remove('md-hide-elm');
  } else {
    eventRecurrenceEditorElm.classList.add('md-hide-elm');
  }
}

function toggleRecurrenceText(repeat) {
  document.querySelectorAll('.md-recurrence-text').forEach(function (elm) {
    if (elm.classList.contains('md-recurrence-' + repeat)) {
      elm.classList.remove('md-hide-elm');
    } else {
      elm.classList.add('md-hide-elm');
    }
  });
}

function navigateToEvent(event) {
  var d = new Date(event.start);
  var year = d.getFullYear();
  var month = d.getMonth();
  var day = d.getDate();
  var recurringRule = event.recurring;
  var addMonth = 0;
  var addYear = 0;
  if (recurringRule) {
    var recurringDay = recurringRule.day;
    var recurringMonth = recurringRule.month - 1;
    switch (recurringRule.repeat) {
      case 'monthly':
        if (day > recurringDay) {
          addMonth = recurringRule.interval || 1;
        }
        day = recurringDay;
        break;
      case 'yearly':
        if (month > recurringMonth || (month === recurringMonth - 1 && day > recurringDay)) {
          addYear = recurringRule.interval || 1;
        }
        day = recurringDay;
        month = recurringMonth;
        break;
    }
  }
  calendar.navigate(new Date(year + addYear, month + addMonth, day, d.getHours()));
}

function updateRecurringEvent() {
  var editFromPopup = addEditPopup.isVisible();

  var updatedEvent = {
    title: eventTitle,
    description: eventDescription,
    allDay: eventAllDay,
    color: eventColor,
    start: eventStart,
    end: eventEnd,
  };

  if (recurrenceEditMode !== 'current') {
    updatedEvent.id = eventId;
    updatedEvent.recurring = getRecurrenceRule();
    updatedEvent.recurringException = eventRecurringException;
  }

  var result = mobiscroll.updateRecurringEvent(
    originalRecurringEvent,
    eventOccurrence,
    editFromPopup ? null : newEvent,
    editFromPopup ? updatedEvent : null,
    recurrenceEditMode,
  );

  if (result.newEvent) {
    calendar.addEvent(result.newEvent);
  }

  calendar.updateEvent(result.updatedEvent);
}

function deleteRecurringEvent() {
  switch (recurrenceEditMode) {
    case 'following':
      var d = new Date(eventStart);
      originalRecurringEvent.recurring.until = new Date(d.getFullYear(), d.getMonth(), d.getDate());
      calendar.updateEvent(originalRecurringEvent);
      break;
    case 'all':
      calendar.removeEvent(originalRecurringEvent);
      break;
    default:
      eventRecurringException.push(eventStart);
      originalRecurringEvent.recurringException = eventRecurringException;
      calendar.updateEvent(originalRecurringEvent);
  }
}

// Fills the popup with the event's data
function fillPopup(event) {
  // Load event properties
  eventId = event.id;
  eventTitle = event.title || '';
  eventDescription = event.description || '';
  eventAllDay = event.allDay;
  eventStart = event.start;
  eventEnd = event.end;
  eventColor = event.color;
  eventRecurringException = event.recurringException || [];
  eventRecurrence = getEventRecurrence(event);

  // Load recurrence rule properties, with default values
  var recurringRule = event.recurring || {};
  recurrenceRepeat = recurringRule.repeat || 'daily';
  recurrenceInterval = recurringRule.interval || 1;
  recurrenceCondition = recurringRule.until ? 'until' : recurringRule.count ? 'count' : 'never';
  recurrenceMonth = recurringRule.month || 1;
  recurrenceDay = recurringRule.day || 1;
  recurrenceWeekDays = recurringRule.weekDays || 'SU';
  recurrenceCount = recurringRule.count || 10;
  recurrenceUntil = recurringRule.until;

  // Set event fields
  mobiscroll.getInst(eventTitleElm).value = eventTitle;
  mobiscroll.getInst(eventDescriptionElm).value = eventDescription;
  mobiscroll.getInst(eventAllDayElm).value = eventAllDay;
  eventStartEndPicker.setVal([eventStart, eventEnd]);
  eventRecurrenceSelect.setOptions({ data: getRecurrenceTypes(eventStart, eventRecurrence) });
  eventRecurrenceSelect.setVal(eventRecurrence);
  toggleDatetimePicker(eventAllDay);
  toggleRecurrenceEditor(eventRecurrence);

  // Set custom recurring rule field values
  recurrenceIntervalElm.value = recurrenceInterval;
  recurrenceCountElm.value = recurrenceCount;
  recurrenceMonthSelect.setVal(recurrenceMonth);
  recurrenceDaySelect.setVal(recurrenceDay);
  recurrenceMonthDaySelect.setVal(recurrenceDay);
  recurrenceUntilDatepicker.setVal(recurrenceUntil);
  mobiscroll.getInst(document.getElementById('recurrence-repeat-' + recurrenceRepeat)).checked = true;
  mobiscroll.getInst(document.getElementById('recurrence-condition-' + recurrenceCondition)).checked = true;
  recurrenceWeekDaysElms.forEach(function (elm) {
    mobiscroll.getInst(elm).checked = recurrenceWeekDays.indexOf(elm.value) !== -1;
  });
  toggleRecurrenceText(recurrenceRepeat);
}

function createAddPopup(event, target) {
  var success = false;

  // Hide delete button inside add popup
  eventDeleteButtonElm.parentElement.classList.add('md-hide-elm');

  // Set popup header text and buttons
  addEditPopup.setOptions({
    anchor: target,
    headerText: 'New event',
    buttons: [
      'cancel',
      {
        text: 'Add',
        keyCode: 'enter',
        handler: function () {
          var newEvent = {
            id: eventId,
            title: eventTitle,
            description: eventDescription,
            allDay: eventAllDay,
            start: eventStart,
            end: eventEnd,
            recurring: getRecurrenceRule(),
          };
          calendar.updateEvent(newEvent);
          navigateToEvent(newEvent);
          success = true;
          addEditPopup.close();
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
    onClose: function () {
      // Remove event if popup is cancelled
      if (!success) {
        calendar.removeEvent(event);
      }
    },
  });

  fillPopup(event);
  addEditPopup.open();
}

function createEditPopup(event, target) {
  // Show delete button inside edit popup
  eventDeleteButtonElm.parentElement.classList.remove('md-hide-elm');

  editedEvent = event;
  originalRecurringEvent = event.recurring ? event.original : null;
  eventOccurrence = event;

  // Set popup header text and buttons
  addEditPopup.setOptions({
    headerText: 'Edit event',
    anchor: target,
    buttons: [
      'cancel',
      {
        text: 'Save',
        keyCode: 'enter',
        handler: function () {
          if (originalRecurringEvent) {
            // TODO: don't allow current update if recurring rule is changed
            createRecurrenceEditPopup(false);
          } else {
            var updatedEvent = {
              id: eventId,
              title: eventTitle,
              description: eventDescription,
              allDay: eventAllDay,
              start: eventStart,
              end: eventEnd,
              recurring: getRecurrenceRule(),
            };
            calendar.updateEvent(updatedEvent);
            navigateToEvent(updatedEvent);
            addEditPopup.close();
          }
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
  });

  fillPopup(event);
  addEditPopup.open();
}

function createRecurrenceEditPopup(isDelete) {
  recurrenceEditModeTextElm.textContent = isDelete ? 'Delete' : 'Edit';
  recurrenceDelete = isDelete;
  recurrenceEditModePopup.open();
}

var MAX_MONTH_DAYS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var DAY_NAMES_SHORT = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
var DAY_NAMES_MAP = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 };
var MONTH_NAMES = [
  { value: 1, text: 'January' },
  { value: 2, text: 'February' },
  { value: 3, text: 'March' },
  { value: 4, text: 'April' },
  { value: 5, text: 'May' },
  { value: 6, text: 'June' },
  { value: 7, text: 'July' },
  { value: 8, text: 'August' },
  { value: 9, text: 'September' },
  { value: 10, text: 'October' },
  { value: 11, text: 'November' },
  { value: 12, text: 'December' },
];

var originalRecurringEvent;
var eventOccurrence;
var newEvent;
var editedEvent;

var eventId;
var eventTitle;
var eventDescription;
var eventAllDay;
var eventStart;
var eventEnd;
var eventColor;
var eventRecurrence;
var eventRecurringException;

var recurrenceRepeat;
var recurrenceInterval;
var recurrenceCondition;
var recurrenceMonth;
var recurrenceDay;
var recurrenceWeekDays;
var recurrenceCount;
var recurrenceUntil;
var recurrenceDelete;
var recurrenceEditMode = 'current';

var eventTitleElm = document.getElementById('popup-event-title');
var eventDescriptionElm = document.getElementById('popup-event-description');
var eventAllDayElm = document.getElementById('popup-event-all-day');
var eventDeleteButtonElm = document.getElementById('popup-event-delete');
var eventRecurrenceEditorElm = document.getElementById('popup-event-recurrence-editor');

var recurrenceIntervalElm = document.getElementById('recurrence-interval');
var recurrenceCountElm = document.getElementById('recurrence-count');
var recurrenceEditModeTextElm = document.getElementById('recurrence-edit-mode-text');
var recurrenceWeekDaysElms = document.querySelectorAll('.md-recurrence-weekdays');

var myEvents = [
  {
    id: 1,
    start: '2026-10-21T00:00',
    end: '2026-10-24T00:00',
    title: 'Alice OFF',
    allDay: true,
    color: '#67ab0d',
  },
  {
    id: 2,
    start: '18:00',
    end: '20:00',
    recurring: {
      repeat: 'weekly',
      weekDays: 'MO',
    },
    title: 'Football training',
    allDay: false,
    color: '#fd966a',
  },
  {
    id: 3,
    start: '17:00',
    end: '17:30',
    recurring: {
      repeat: 'monthly',
      day: 1,
    },
    title: 'Rent payment',
    allDay: false,
    color: '#3ea39e',
  },
  {
    id: 4,
    recurring: {
      repeat: 'yearly',
      day: 6,
      month: 5,
    },
    title: 'Dexter BD',
    allDay: true,
    color: '#d00f0f',
  },
];

// Init the event calendar
var calendar = mobiscroll.eventcalendar('#demo-add-delete-event', {
  clickToCreate: 'double',
  dragToCreate: true,
  dragToMove: true,
  dragToResize: true,
  view: {
    calendar: { labels: true },
  },
  data: myEvents,
  onEventClick: function (args) {
    createEditPopup(args.event, args.domEvent.currentTarget);
  },
  onEventUpdate: function (args) {
    var event = args.event;
    if (event.recurring) {
      originalRecurringEvent = args.oldEvent;
      eventOccurrence = args.oldEventOccurrence;
      if (args.isDelete) {
        eventRecurringException = originalRecurringEvent.recurringException || [];
        eventStart = eventOccurrence.start;
        createRecurrenceEditPopup(true);
      } else {
        createRecurrenceEditPopup(false);
      }
      return false;
    }
  },
  onEventCreate: function (args) {
    if (args.originEvent) {
      // Store created event on recurring occurrence drag
      newEvent = args.event;
      // Prevent event creation on recurring occurrence drag
      return false;
    }
  },
  onEventCreated: function (args) {
    createAddPopup(args.event, args.target);
  },
});

// Init event start/end date picker
var eventStartEndPicker = mobiscroll.datepicker('#popup-event-dates', {
  controls: ['date'],
  select: 'range',
  startInput: '#popup-event-start',
  endInput: '#popup-event-end',
  showRangeLabels: false,
  touchUi: true,
  responsive: { medium: { touchUi: false } },
  onChange: function (args) {
    var dates = args.value;
    eventStart = dates[0];
    eventEnd = dates[1];
    eventRecurrenceSelect.setOptions({ data: getRecurrenceTypes(eventStart) });
  },
});

// Init event recurrence select
var eventRecurrenceSelect = mobiscroll.select('#popup-event-recurrence', {
  data: [],
  touchUi: true,
  responsive: { small: { touchUi: false } },
  onChange: function (args) {
    eventRecurrence = args.value;
    toggleRecurrenceEditor(eventRecurrence);
  },
});

// Init recurring event month day selection for monthly recurrence
var recurrenceDaySelect = mobiscroll.select('#recurrence-day', {
  data: getMonthDays(1),
  touchUi: true,
  responsive: { small: { touchUi: false } },
  maxWidth: 80,
  onChange: function (args) {
    recurrenceDay = args.value;
  },
});

// Init recurring rule month selection for yearly recurrence
var recurrenceMonthSelect = mobiscroll.select('#recurrence-month', {
  data: MONTH_NAMES,
  touchUi: true,
  responsive: { small: { touchUi: false } },
  onChange: function (args) {
    recurrenceMonth = args.value;
    var maxDay = MAX_MONTH_DAYS[recurrenceMonth - 1];
    if (recurrenceDay > maxDay) {
      recurrenceMonthDaySelect.setVal(maxDay);
    }
    recurrenceMonthDaySelect.setOptions({ data: getMonthDays(recurrenceMonth) });
  },
});

// Init recurring event month day selection for yearly recurrence
var recurrenceMonthDaySelect = mobiscroll.select('#recurrence-month-day', {
  data: getMonthDays(1),
  touchUi: true,
  responsive: { small: { touchUi: false } },
  maxWidth: 80,
  onChange: function (args) {
    recurrenceDay = args.value;
  },
});

// Init the until datepicker
var recurrenceUntilDatepicker = mobiscroll.datepicker('#recurrence-until', {
  controls: ['calendar'],
  display: 'anchored',
  touchUi: false,
  dateFormat: 'YYYY-MM-DD',
  returnFormat: 'iso8601',
  onChange: function (args) {
    recurrenceUntil = args.value;
  },
  onOpen: function () {
    // Check the until stop condition radio
    recurrenceCondition = 'until';
    mobiscroll.getInst(document.getElementById('recurrence-condition-until')).checked = true;
  },
});

// Init popup for event create/edit
var addEditPopup = mobiscroll.popup('#demo-add-edit-popup', {
  display: 'bottom',
  contentPadding: false,
  fullScreen: true,
  scrollLock: false,
  height: 500,
  responsive: {
    medium: {
      display: 'anchored',
      width: 510,
      fullScreen: false,
      touchUi: false,
    },
  },
  cssClass: 'md-recurring-event-editor-popup',
});

// Init recurring edit mode popup
var recurrenceEditModePopup = mobiscroll.popup('#demo-recurrence-edit-mode-popup', {
  display: 'bottom',
  contentPadding: false,
  buttons: [
    'cancel',
    {
      text: 'Ok',
      keyCode: 'enter',
      handler: function () {
        if (recurrenceDelete) {
          deleteRecurringEvent();
        } else {
          updateRecurringEvent();
        }
        addEditPopup.close();
        recurrenceEditModePopup.close();
      },
      cssClass: 'mbsc-popup-button-primary',
    },
  ],
  onClose: function () {
    // Reset edit mode to current
    recurrenceEditMode = 'current';
    mobiscroll.getInst(document.getElementById('recurrence-edit-mode-current')).checked = true;
  },
  responsive: {
    medium: {
      display: 'center',
      fullScreen: false,
      touchUi: false,
    },
  },
  cssClass: 'md-recurring-event-editor-popup',
});

// Attach event handlers

eventTitleElm.addEventListener('input', function (ev) {
  eventTitle = ev.target.value;
});

eventDescriptionElm.addEventListener('change', function (ev) {
  eventDescription = ev.target.value;
});

eventAllDayElm.addEventListener('change', function (ev) {
  eventAllDay = ev.target.checked;
  toggleDatetimePicker(eventAllDay);
});

eventDeleteButtonElm.addEventListener('click', function () {
  if (editedEvent.recurring) {
    createRecurrenceEditPopup(true);
  } else {
    calendar.removeEvent(editedEvent);
    addEditPopup.close();
  }
});

recurrenceWeekDaysElms.forEach(function (elm) {
  elm.addEventListener('change', function () {
    var values = [];
    recurrenceWeekDaysElms.forEach(function (ev) {
      if (ev.checked) {
        values.push(ev.value);
      }
    });
    recurrenceWeekDays = values.join(',');
  });
});

recurrenceIntervalElm.addEventListener('change', function (ev) {
  var value = +ev.target.value;
  recurrenceInterval = !value || value < 1 ? 1 : value;
  ev.target.value = recurrenceInterval;
});

recurrenceCountElm.addEventListener('change', function (ev) {
  var value = +ev.target.value;
  recurrenceCount = !value || value < 1 ? 1 : value;
  ev.target.value = recurrenceCount;
});

recurrenceCountElm.addEventListener('click', function () {
  // Check the count stop condition radio
  recurrenceCondition = 'count';
  mobiscroll.getInst(document.getElementById('recurrence-condition-count')).checked = true;
});

document.querySelectorAll('.md-recurrence-repeat').forEach(function (elm) {
  elm.addEventListener('change', function (ev) {
    recurrenceRepeat = ev.target.value;
    toggleRecurrenceText(recurrenceRepeat);
  });
});

document.querySelectorAll('.md-recurrence-edit-mode').forEach(function (elm) {
  elm.addEventListener('change', function (ev) {
    recurrenceEditMode = ev.target.value;
  });
});

document.querySelectorAll('.md-recurrence-condition').forEach(function (elm) {
  elm.addEventListener('change', function (ev) {
    recurrenceCondition = ev.target.value;
  });
});
```

### HTML

```html
<div id="demo-add-delete-event"></div>

<div style="display: none;">
    <div id="demo-add-edit-popup">
        <div class="mbsc-form-group">
            <label>
                Title
                <input mbsc-input id="popup-event-title" />
            </label>
            <label>
                Description
                <textarea mbsc-textarea id="popup-event-description"></textarea>
            </label>
        </div>
        <div class="mbsc-form-group">
            <label>
                <input mbsc-switch data-label="All-day" id="popup-event-all-day" type="checkbox" />
            </label>
            <label>
                <input mbsc-input data-label="Starts" id="popup-event-start" />
            </label>
            <label>
                <input mbsc-input data-label="Ends" id="popup-event-end" />
            </label>
            <div id="popup-event-dates"></div>
            <label>
                <input mbsc-input data-label="Repeats" data-dropdown="true" id="popup-event-recurrence" />
            </label>
        </div>

        <div id="popup-event-recurrence-editor">
            <div mbsc-segmented-group>
                <label>
                    Daily
                    <input mbsc-segmented id="recurrence-repeat-daily" class="md-recurrence-repeat" name="recurrence-repeat" type="radio" value="daily" checked />
                </label>
                <label>
                    Weekly
                    <input mbsc-segmented id="recurrence-repeat-weekly" class="md-recurrence-repeat" name="recurrence-repeat" type="radio" value="weekly" />
                </label>
                <label>
                    Monthly
                    <input mbsc-segmented id="recurrence-repeat-monthly" class="md-recurrence-repeat" name="recurrence-repeat" type="radio" value="monthly" />
                </label>
                <label>
                    Yearly
                    <input mbsc-segmented id="recurrence-repeat-yearly" class="md-recurrence-repeat" name="recurrence-repeat" type="radio" value="yearly" />
                </label>
            </div>

            <div class="md-recurrence-options">
                <span>Repeat every</span>
                <span class="md-recurrence-input md-recurrence-input-nr">
                    <label>
                        <input id="recurrence-interval" mbsc-input data-input-style="outline" type="number" min="1" />
                    </label>
                </span>

                <span class="md-recurrence-text md-recurrence-daily">day(s)</span>
                <span class="md-recurrence-text md-recurrence-weekly">week(s) on</span>

                <span class="md-recurrence-text md-recurrence-monthly">
                    month(s) on day
                    <span class="md-recurrence-input md-recurrence-input-nr">
                        <label>
                            <input id="recurrence-day" mbsc-input data-dropdown="true" data-input-style="outline" />
                        </label>
                    </span>
                </span>

                <span class="md-recurrence-text md-recurrence-yearly">
                    year(s) <br />on day
                    <span class="md-recurrence-input md-recurrence-input-nr">
                        <label>
                            <input id="recurrence-month-day" mbsc-input data-dropdown="true" data-input-style="outline" />
                        </label>
                    </span>
                <span>of</span>
                <span class="md-recurrence-input">
                        <label>
                            <input id="recurrence-month" mbsc-input data-dropdown="true" data-input-style="outline" />
                        </label>
                    </span>
                </span>
                <div>
                    <p class="md-recurrence-desc md-recurrence-text md-recurrence-daily">The event will be repeated every day or every x days, depending on the value</p>
                    <p class="md-recurrence-desc md-recurrence-text md-recurrence-weekly">The event will be repeated every x weeks on specific weekdays</p>
                    <p class="md-recurrence-desc md-recurrence-text md-recurrence-monthly">The event will be repeated every x months on specific day of the month</p>
                    <p class="md-recurrence-desc md-recurrence-text md-recurrence-yearly">The event will be repeated every x years on specific day of a specific month</p>
                </div>
            </div>

            <div class="md-recurrence-text md-recurrence-weekly" mbsc-segmented-group>
                <label>
                    Sun
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="SU" />
                </label>
                <label>
                    Mon
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="MO" />
                </label>
                <label>
                    Tue
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="TU" />
                </label>
                <label>
                    Wed
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="WE" />
                </label>
                <label>
                    Thu
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="TH" />
                </label>
                <label>
                    Fri
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="FR" />
                </label>
                <label>
                    Sat
                    <input mbsc-segmented class="md-recurrence-weekdays" type="checkbox" value="SA" />
                </label>
            </div>

            <div class="md-recurrence-ends">Ends</div>

            <div class="mbsc-form-group">
                <label>
                    <input mbsc-radio id="recurrence-condition-never" class="md-recurrence-condition" type="radio" name="recurrence-condition" value="never" data-label="Never" data-position="start" data-description="The event will repeat indefinitely" checked />
                </label>
                <label>
                    <input mbsc-radio id="recurrence-condition-until" class="md-recurrence-condition" type="radio" name="recurrence-condition" value="until" data-position="start" data-description="The event will run until it reaches a specific date" />
                    Until
                    <span class="md-recurrence-input">
                        <label>
                            <input id="recurrence-until" mbsc-input data-input-style="outline" placeholder="Select a date" />
                        </label>
                </span>
                </label>
                <label>
                    <input mbsc-radio id="recurrence-condition-count" class="md-recurrence-condition" type="radio" name="recurrence-condition" value="count" data-position="start" data-description="The event will repeat until it reaches a certain amount of occurrences" />
                    After
                    <span class="md-recurrence-input md-recurrence-input-nr">
                        <label>
                            <input id="recurrence-count" mbsc-input data-input-style="outline" type="number" min="1" />
                        </label>
                </span>
                occurrences
                </label>
            </div>
        </div>

        <div class="mbsc-button-group">
            <button class="mbsc-button-block" id="popup-event-delete" mbsc-button data-color="danger" data-variant="outline">Delete event</button>
        </div>
    </div>
    <div id="demo-recurrence-edit-mode-popup">
        <div class="mbsc-form-group">
            <div class="mbsc-form-group-title"><span id="recurrence-edit-mode-text"></span> recurring event</div>
            <label>
                <input mbsc-radio id="recurrence-edit-mode-current" class="md-recurrence-edit-mode" data-label="This event only" value="current" name="recurrence-edit-mode" type="radio" checked />
            </label>
            <label>
                <input mbsc-radio id="recurrence-edit-mode-following" class="md-recurrence-edit-mode" data-label="This and following events" value="following" name="recurrence-edit-mode" type="radio" />
            </label>
            <label>
                <input mbsc-radio id="recurrence-edit-mode-all" class="md-recurrence-edit-mode" data-label="All events" value="all" name="recurrence-edit-mode" type="radio" />
            </label>
        </div>
    </div>
</div>
```

### CSS

```css
.md-recurring-event-editor-popup .md-recurrence-input .mbsc-textfield-wrapper {
    display: inline-block;
    margin: 3px;
    z-index: 3;
    line-height: normal;
}

.md-recurring-event-editor-popup .md-recurrence-input .mbsc-textfield {
    width: 140px;
    height: 32px;
}

.md-recurring-event-editor-popup .md-recurrence-input-nr .mbsc-textfield {
    width: 90px;
}

.md-recurring-event-editor-popup .md-recurrence-input .mbsc-select-icon {
    top: 50%;
    height: 1.5em;
    margin-top: -0.75em;
}

.md-recurrence-options {
    padding: 0 1em;
    margin: 1em 0;
}

.md-recurrence-ends {
    padding: 0 1em;
    margin: 1.25em 0 -1.25em 0;
}

.md-recurrence-desc {
    font-size: 12px;
    font-weight: normal;
    color: #555;
    margin: 1em 0;
}

.md-hide-elm {
    display: none;
}
```

## Implementation notes

- Use `view: { calendar: { labels: true } }` with `clickToCreate: 'double'`, `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`. Pre-load 4 default recurring events: "Take vitamins" (daily every day, 10:00-12:00, `#67ab0d`), "Football training" (weekly Monday, 18:00-20:00, `#fd966a`), "Rent payment" (monthly on day 10, 17:00-18:00, `#3ea39e`), "Dexter BD" (yearly June 6, all-day, `#d00f0f`).
- The **add/edit popup** (`Popup`, responsive: anchored 510px on medium, bottom full-screen on mobile) contains: **Title** (`Input`), **Description** (`Textarea`), **All-day** (`Switch`), **Start/End** (`Datepicker` range with `startInput`/`endInput`; controls: `['calendar', 'time']` normally, `['calendar']` all-day; mobile: `['datetime']` / `['date']`), **Repeats** (`Select`), the **custom recurrence editor** section (shown only when repeat is `'custom'` or `'custom-value'`), and a **Delete event** button (edit mode only).
- The **Repeats `Select`** data has these preset values whose display texts update dynamically based on the event's start date: `norepeat` ("Does not repeat"), `daily` ("Daily"), `weekly` ("Weekly on [day name]"), `monthly` ("Monthly on day [N]"), `monthly-pos` ("Monthly on the [ordinal] [day name]"), `yearly` ("Annually on [Month] [day]"), `yearly-pos` ("Annually on the [ordinal] [day name] of [Month]"), `weekday` ("Every weekday (Monday to Friday)"), `custom` ("Custom"). When an existing event has a custom rule with `interval`/`count`/`until`, a `custom-value` entry with generated descriptive text is inserted dynamically.
- **`loadPopupForm(event)`**: populates all popup fields from the event object; sets the default `untilDate` to the day after `event.start`; rebuilds the Repeats data with date-specific labels. For recurring events: if the rule has `interval`/`count`/`until` set `selectedRepeat = 'custom-value'` and add the generated text entry; if `weekDays === 'MO,TU,WE,TH,FR'` → `'weekday'`; if positional (`pos`) → `repeat + '-pos'`; otherwise → `repeat`. Calls `resetCustomValues()` for non-recurring events.
- **Custom recurrence editor**: a `SegmentedGroup` (Daily/Weekly/Monthly/Yearly) picks `repeatType`. Below it: "Repeat every [interval `Input`] [days/weeks/months/years]". For Weekly: a multi-select weekday `SegmentedGroup` (Sun-Sat). For Monthly: day `Select` (1-31). For Yearly: day `Select` + month `Select`. An **Ends** section with a `RadioGroup`: "Never" / "Until [date `Datepicker`]" / "After [count `Input`] occurrences".
- **`getCustomRule()`**: builds the `recurring` object to save on the event. Presets map directly using the event's start date for weekday/day/month values. `weekday` → `{ repeat: 'weekly', weekDays: 'MO,TU,WE,TH,FR' }`. `custom`/`custom-value` → `{ repeat: repeatType, interval: repeatNr, ...weekDays/day/month as needed, ...until/count from condition }`. Returns `null`/`undefined` for `norepeat`.
- `onEventClick` → open popup in **edit mode**: store `originalRecurringEvent = event.original` if recurring, call `loadPopupForm(event)`, set anchor to clicked element, open with "Edit event" header and "Save" button.
- `onEventCreated` → open popup in **add mode**: call `resetCustomValues()`, `loadPopupForm(event)`, open with "New Event" header and "Add" button. **On popup close without confirming** (add mode): refresh the events array to strip the temporary event; for the imperative API, call `inst.removeEvent(event)` in the popup's `onClose` callback.
- `onEventCreate` → if `args.originEvent` is a recurring event: store `args.event` as `newEvent` and `return false` (prevents duplicate creation; the recurrence mode popup will complete the operation).
- `onEventUpdate` → if the updated event is recurring: store `args.oldEvent` as `originalRecurringEvent` and `args.oldEventOccurrence` as `eventOccurrence`; open the **recurrence mode popup** in "Edit" or "Delete" mode (based on `args.isDelete`); `return false`.
- Handle `onEventDeleted` to delete the event by filtering the events array; for the imperative API, handle deletion in the Delete button's click handler with `inst.removeEvent(editedEvent)`.
- **Recurrence mode popup** (separate `Popup`): header "[Edit/Delete] recurring event"; `RadioGroup`: "This event only" / "This and following events" / "All events" (default: `current`); buttons: Cancel / Ok.
  - **Ok → delete** (`deleteRecurringEvent`): `current` - push `event.start` to `recurringException`, update original; `following` - set `original.recurring.until = event.start`, update original; `all` - remove original. Apply changes to the events array; for the imperative API, call `inst.updateEvent(original)` or `inst.removeEvent(original)`.
  - **Ok → edit**: call `updateRecurringEvent(originalRecurringEvent, eventOccurrence, newEvent, updatedEventOrNull, recurringEditMode)`. React imports it from `@mobiscroll/react`, Vue from `@mobiscroll/vue`, Angular from `@mobiscroll/angular`, JS/jQuery use `mobiscroll.updateRecurringEvent`. Returns `{ updatedEvent, newEvent? }` - replace the original in the list and, if `newEvent` exists, append it; for the imperative API, call `inst.updateEvent(result.updatedEvent)` and `inst.addEvent(result.newEvent)`.
  - When triggered from the **popup Save button** (edit path): pass the form-built event as the 4th argument and `null` as the 3rd. When triggered from **drag/drop** (`onEventUpdate` path): pass `null` as 4th and the drag-created `newEvent` as 3rd.
- **Add button**: build event from form state with `getCustomRule()` as `recurring`, append to the events list; JS/jQuery: call `inst.updateEvent(newEvent)` to finalize the temp event. Then call `navigateTo()` and close popup.
- **Save button (non-recurring)**: build updated event from form state, replace in list, call `navigateTo()`, close popup.
- **`navigateTo()`**: for yearly events, compute the correct year and navigate to `recurring.month`/`recurring.day`; for all others, navigate to `event.start`.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-javascript.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config javascript
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config javascript --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/javascript/getting-started/installation

## Related demos

- Previous: [Add, Edit and Delete Events](https://demo.mobiscroll.com/javascript/eventcalendar/create-read-update-delete-CRUD.md) - Add full event management to a Javascript event calendar — create events by drag or click, edit them in a custom dialog, and delete with undo.
- Next: [Block Past Event Creation](https://demo.mobiscroll.com/javascript/eventcalendar/disallow-past-event-creation.md) - Keep past dates read-only in a Javascript event calendar — block creation and editing on past days and show toast feedback on failed attempts.
