JavaScript Timeline Meal Planner

Use the timeline summary mode/event listing to create a weekly meal planner. Meal types are represented as resources with a custom template. Events are the actual meals with custom properties, like calories or notes.

The add/edit form shows up in a custom popover that opens on double click for meal creation and on click for editing.

The popover header is customized with a custom header renderer function using the renderHeader option.

By default the second dimension of the timeline (vertical axis) is reserved for resources, however it can be configured and used as "time slots" if the times are not relevant.

Download and try example

Demo

Loading the live demo

Preview in full screen

Build a weekly meal planner with the Timeline — meal types as color-coded resource rows, custom add/edit popover for creating or updating meals.

Code

mobiscroll.setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
});

var calendar;
var popup;
var oldMeal;
var tempMeal;
var deleteMeal;
var restoreMeal;
var formatDate = mobiscroll.formatDate;
var nameInput = document.getElementById('meal-name-input');
var caloriesInput = document.getElementById('meal-calories-input');
var notesTextarea = document.getElementById('meal-notes-textarea');
var deleteButton = document.getElementById('meal-delete');
var typeCont = document.getElementById('meal-type-segmented');
var typesSegmented;

var types = [
  {
    id: 1,
    name: 'Breakfast',
    color: '#e20f0f',
    kcal: '300 - 400 kcal',
    icon: '🍳',
  },
  {
    id: 2,
    name: 'Elevenses',
    color: '#157d13',
    kcal: '100 - 200 kcal',
    icon: '🍌',
  },
  {
    id: 3,
    name: 'Lunch',
    color: '#32a6de',
    kcal: '500 - 700 kcal',
    icon: '🍜',
  },
  {
    id: 4,
    name: 'Dinner',
    color: '#e29d1d',
    kcal: '400 - 600 kcal',
    icon: '🥙',
  },
  {
    id: 5,
    name: 'Snack',
    color: '#68169c',
    kcal: '100 - 200 kcal',
    icon: '🥨',
  },
];

function addMealPopup() {
  // Hide delete button inside add popup
  deleteButton.style.display = 'none';

  deleteMeal = true;
  restoreMeal = false;

  // Set popup header text and buttons for adding
  popup.setOptions({
    renderHeader: function () {
      return (
        '<div class="md-meal-planner-header-primary">New meal</div>' +
        '<div class="md-meal-planner-header-date">' +
        formatDate('DDDD, DD MMMM YYYY', new Date(tempMeal.start)) +
        '</div>'
      );
    },
    buttons: [
      'cancel',
      {
        text: 'Add',
        keyCode: 'enter',
        handler: function () {
          calendar.updateEvent(tempMeal);

          deleteMeal = false;
          popup.close();
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
  });

  // Fill popup with a new event data
  mobiscroll.getInst(nameInput).value = tempMeal.title;
  mobiscroll.getInst(caloriesInput).value = '';
  mobiscroll.getInst(notesTextarea).value = '';

  typesSegmented.forEach(function (elm) {
    mobiscroll.getInst(elm).checked = +elm.value == tempMeal.resource;
  });

  popup.open();
}

function editMealPopup(args) {
  var ev = args.event;
  var resource = args.resourceObj;

  // Show delete button inside edit popup
  deleteButton.style.display = 'block';

  deleteMeal = false;
  restoreMeal = true;

  // Set popup header text and buttons for editing
  popup.setOptions({
    renderHeader: function () {
      return (
        '<div class="md-meal-planner-header-primary">' +
        resource.name +
        '</div>' +
        '<div class="md-meal-planner-header-date">' +
        formatDate('DDDD, DD MMMM YYYY', new Date(tempMeal.start)) +
        '</div>'
      );
    },
    buttons: [
      'cancel',
      {
        text: 'Save',
        keyCode: 'enter',
        handler: function () {
          // Update event with the new properties on save button click
          calendar.updateEvent({
            id: ev.id,
            title: tempMeal.title,
            calories: tempMeal.calories,
            notes: tempMeal.notes,
            start: ev.start,
            end: ev.end,
            resource: tempMeal.resource,
          });

          restoreMeal = false;
          popup.close();
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
  });

  // Fill popup with the selected event data
  mobiscroll.getInst(nameInput).value = ev.title || '';
  mobiscroll.getInst(caloriesInput).value = ev.calories || '';
  mobiscroll.getInst(notesTextarea).value = ev.notes || '';

  typesSegmented.forEach(function (elm) {
    mobiscroll.getInst(elm).checked = +elm.value == tempMeal.resource;
  });

  popup.open();
}

calendar = mobiscroll.eventcalendar('#demo-meal-planner', {
  view: {
    timeline: {
      type: 'week',
      eventDisplay: 'fill',
      startDay: 1,
      endDay: 5,
    },
  },
  resources: types,
  dragToCreate: false,
  dragToResize: false,
  dragToMove: true,
  clickToCreate: true,
  extendDefaultEvent: function () {
    return {
      title: 'New meal',
    };
  },
  onEventCreated: function (args) {
    // Store temporary event
    tempMeal = args.event;
    setTimeout(function () {
      addMealPopup();
    }, 100);
  },
  onEventClick: function (args) {
    oldMeal = Object.assign({}, args.event);
    tempMeal = args.event;

    if (!popup.isVisible()) {
      editMealPopup(args);
    }
  },
  renderResource: function (resource) {
    return (
      '<div class="md-meal-planner-cont">' +
      '<div class="md-meal-planner-title" style="color:' +
      resource.color +
      '">' +
      '<span class="md-meal-planner-icon">' +
      resource.icon +
      '</span>' +
      resource.name +
      '</div>' +
      '<div class="md-meal-planner-kcal">' +
      resource.kcal +
      '</div>' +
      '</div>'
    );
  },
  renderTimelineEventContent: function (args) {
    var event = args.original;
    return (
      '<div class="md-meal-planner-event">' +
      '<div class="md-meal-planner-event-title">' +
      event.title +
      '</div>' +
      (event.calories ? '<div class="md-meal-planner-event-desc">Calories: ' + event.calories + ' kcal</div>' : '') +
      '</div>'
    );
  },
});

mobiscroll.getJson(
  'https://trial.mobiscroll.com/meal-planner/',
  function (events) {
    calendar.setEvents(events);
  },
  'jsonp',
);

popup = mobiscroll.popup('#meal-planner-popup', {
  display: 'bottom',
  contentPadding: false,
  fullScreen: true,
  onClose: function () {
    if (deleteMeal) {
      calendar.removeEvent(tempMeal);
    } else if (restoreMeal) {
      calendar.updateEvent(oldMeal);
    }
  },
  responsive: {
    medium: {
      display: 'center',
      width: 400,
      fullScreen: false,
      touchUi: false,
      showOverlay: false,
    },
  },
});

function appendTypes() {
  var segmented = '<div mbsc-segmented-group>';

  for (var i = 0; i < types.length; ++i) {
    var type = types[i];
    segmented +=
      '<label>' +
      type.name +
      '<input type="radio" mbsc-segmented name="meal-planner-type" value="' +
      type.id +
      '" class="meal-planner-type"></label>';
  }

  segmented += '</div>';
  typeCont.innerHTML = segmented;
  mobiscroll.enhance(typeCont);
  typesSegmented = document.querySelectorAll('input[name=meal-planner-type]');

  typesSegmented.forEach(function (elm) {
    elm.addEventListener('change', function (ev) {
      tempMeal.resource = +ev.target.value;
    });
  });
}

appendTypes();

nameInput.addEventListener('change', function (ev) {
  tempMeal.title = ev.target.value;
});

caloriesInput.addEventListener('change', function (ev) {
  tempMeal.calories = ev.target.value;
});

notesTextarea.addEventListener('change', function (ev) {
  tempMeal.notes = ev.target.value;
});

deleteButton.addEventListener('click', function () {
  // Delete current event on button click
  calendar.removeEvent(tempMeal);
  popup.close();

  // Save a local reference to the deleted event
  var deletedMeal = tempMeal;

  mobiscroll.snackbar({
    button: {
      action: function () {
        calendar.addEvent(deletedMeal);
      },
      text: 'Undo',
    },
    message: 'Event deleted',
  });
});
  

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.