---
title: "Javascript Timeline Outlook Calendar Sync Example | Mobiscroll"
component: "Timeline"
framework: "JavaScript"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/javascript/timeline/sync-events-outlook-calendar"
---

# Outlook Calendar Sync - JavaScript Timeline demo

Display Outlook Calendar events on a Javascript Timeline — each calendar as a separate resource row. Plain JS api for usage everywhere.

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

## Description

The event calendar comes with third party calendar integration support through the [**integration plugin**](https://mobiscroll.com/docs/javascript/eventcalendar/calendar-integrations). This needs to be added to the download package or installed separately from a dedicated NPM package.

It includes everything you need to authenticate your users, get their Outlook calendars and provides functions for CRUD operations. It takes care of loading the events and converts them to the required format so that they can be displayed on the Mobiscroll calendar separately or mixed with other calendars and events.

Customizing the interaction, event workflows, whether the events are read-only is up to you. In this live demo, the different calendars are handled as resources and have their own timeline rows. You can manipulate events in the desired calendars, while read-only calendars are greyed out.

This example uses the [**integration plugin**](https://mobiscroll.com/docs/javascript/eventcalendar/calendar-integrations) to connect to and load events from Outlook Calendars and it is not available in the trial. Give the live demo a try and see how it can be configured.

## What this demo shows

- A desktop weekly timeline where days are arranged horizontally and Outlook calendars are listed as resource rows on the left.
- **Header navigation** The month and year label opens date navigation, the previous and next arrows move between weeks, and the Today button returns to the current date.
- **Signed-out state** No events are loaded by default. The `Sync My Outlook Calendars` button in the header prompts the user to sign in to Outlook Calendar.
- **Authentication flow** Clicking the Outlook sign-in button opens a new browser tab where the user can authenticate with their Outlook account.
- **Signed-in state** After sign-in, the header button changes to `My Calendars`. Clicking it opens an anchored popup with guidance about editing events and syncing changes back to Outlook Calendar.
- **Editing toggle** The authenticated popup includes an `Enable editing` switch. It is off by default and controls whether event creation, moving, resizing, and updating are allowed.
- **Update confirmation** When editing is enabled, updating an event opens a confirmation popup with the message `Are you sure you want to update this event? This action will affect your Outlook Calendar event.`
- **Update actions** The confirmation popup includes `Cancel` and `Update` buttons. `Cancel` closes the popup and restores the original event, while `Update` applies the change in the Mobiscroll timeline and syncs it back to Outlook Calendar.
- **Sync feedback** Creating, updating, or deleting an event shows a bottom-centered toast confirming the change in the selected Outlook Calendar.
- **Calendar list** The `My Calendars` popup shows multiple calendar switches, such as the user's personal calendar and country-specific holiday calendars.
- **Sign-out** The bottom of the popup includes a `Log out of my account` button for disconnecting the Outlook Calendar account.
- **Week view** The strip below the header shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Resources** After sign-in, Outlook calendars appear as timeline resources, with each calendar shown as a separate row.
- **Readonly calendars** Calendars without write access are treated as readonly rows and are greyed out.
- **Event labels** Events appear as colored labels with the event title shown in bold.
- **Date positioning** Events are positioned by their assigned calendar resource and exact date range.
- **Event interaction** Events highlight on hover and show drag and resize handles when editing is enabled.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created by double-clicking the timeline or by clicking and dragging across a time range when editing is enabled.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through dates and calendar resources.

## Best for

- **Personal calendar scheduling** Building timeline views where users can browse and manage events from their own Outlook Calendar.
- **Outlook Calendar sync workflows** Creating scheduling interfaces where event changes need to sync back to Outlook Calendar.
- **Multi-calendar timelines** Showing several Outlook calendars, such as personal, shared, and holiday calendars, as separate timeline rows.
- **Calendar visibility controls** Letting users toggle individual Outlook calendars on or off inside the same timeline view.
- **Readonly calendar handling** Displaying calendars without write access while preventing edits to those calendar rows.

## 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/eventcalendar/api#method-setOptions) - method
- [`resources`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-resources) - option
- [`data`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) - option
- [`invalid`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-invalid) - option
- [`setEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setEvents) - method
- [`close`](https://mobiscroll.com/docs/javascript/popup/api#method-close) - method
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`exclusiveEndDates`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-exclusiveEndDates) - option
- [`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
- [`renderHeader`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderHeader) - renderer
- [`onPageLoading`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreate) - event
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`addEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-addEvent) - method
- [`onEventUpdate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdate) - event
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`onEventDelete`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDelete) - event
- [`anchor`](https://mobiscroll.com/docs/javascript/popup/api#opt-anchor) - option
- [`width`](https://mobiscroll.com/docs/javascript/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/popup/api#opt-touchUi) - option
- [`showOverlay`](https://mobiscroll.com/docs/javascript/popup/api#opt-showOverlay) - option
- [`scrollLock`](https://mobiscroll.com/docs/javascript/popup/api#opt-scrollLock) - option
- [`display`](https://mobiscroll.com/docs/javascript/popup/api#opt-display) - option
- [`navigate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-navigate) - method
- [`open`](https://mobiscroll.com/docs/javascript/popup/api#method-open) - method

## Code

### JS

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

var outlookCalendarSync = mobiscroll.outlookCalendarSync;

var calendarElm = document.getElementById('demo-sync-events-outlook-calendar');
var popupElm = document.getElementById('demo-sync-events-outlook-calendar-popup');
var editButton = document.getElementById('demo-outlook-cal-edit');

var startDate;
var endDate;
var calendarIds = [];
var events = [];
var readonlyCals = [];
var debounce;
var calendarData = {};
var myResources = [];

function toggleContainers(loggedIn) {
  var loggedOutCont = document.getElementById('outlook-cal-sign-in');
  var loggedInCont = document.getElementById('outlook-cal-get-calendars');

  if (loggedIn) {
    loggedOutCont.style.display = 'none';
    loggedInCont.style.display = 'block';
  } else {
    loggedInCont.style.display = 'none';
    loggedOutCont.style.display = 'block';
  }
}

function loadEvents(checked, calendarId) {
  if (checked) {
    loadingEvents(true);
    calendarIds.push(calendarId);
    outlookCalendarSync
      .getEvents([calendarId], startDate, endDate)
      .then(function (resp) {
        var newResource = calendarData[calendarId];
        var newResources = myResources.slice();
        newResources.push({ id: calendarId, name: newResource.name, color: newResource.color });
        myResources = newResources;
        loadingEvents(false);
        resp.forEach(function (event) {
          event.resource = event.outlookCalendarId;
        });
        events = events.concat(resp);
        inst.setOptions({ resources: newResources, data: events });
      })
      .catch(onError);
  } else {
    var index = calendarIds.indexOf(calendarId);
    var newResources = myResources.slice();
    newResources = newResources.filter(function (c) {
      return c.id !== calendarId;
    });
    myResources = newResources;
    if (index !== -1) {
      calendarIds.splice(index, 1);
    }
    events = events.filter(function (event) {
      return event.outlookCalendarId !== calendarId;
    });
    inst.setOptions({ resources: newResources, data: events });
  }
}

function loadingEvents(show) {
  if (show) {
    calendarElm.classList.add('md-loading-events');
  } else {
    calendarElm.classList.remove('md-loading-events');
  }
}

function onError(resp) {
  mobiscroll.toast({
    message: resp.message,
  });
}

function onSignedIn() {
  toggleContainers(true);

  outlookCalendarSync
    .getCalendars()
    .then(function (calendars) {
      var calList = '';

      calendars.sort(function (c) {
        return c.isDefaultCalendar ? -1 : 1;
      });

      for (var i = 0; i < calendars.length; ++i) {
        var c = calendars[i];
        calList +=
          '<label>' +
          '<input mbsc-switch value="' +
          c.id +
          '" data-label="' +
          c.name +
          '" type="checkbox" class="outlook-calendar-switch" checked />' +
          '</label>';

        calendarIds.push(c.id);
        myResources.push({ id: c.id, name: c.name, color: c.hexColor });
        calendarData[c.id] = { name: c.name, color: c.hexColor };

        if (!c.canEdit) {
          readonlyCals.push(c.id);
        }
      }

      inst.setOptions({
        resources: myResources,
        invalid: [
          {
            recurring: {
              repeat: 'daily',
            },
            resource: readonlyCals,
          },
        ],
      });

      var calendarList = document.getElementById('outlook-cal-list');
      calendarList.innerHTML = calList;

      loadingEvents(true);

      outlookCalendarSync
        .getEvents(calendarIds, startDate, endDate)
        .then(function (resp) {
          loadingEvents(false);
          resp.forEach(function (event) {
            event.resource = event.outlookCalendarId;
          });
          events = resp;
          inst.setEvents(events);
        })
        .catch(onError);

      mobiscroll.enhance(calendarList);
    })
    .catch(onError);
}

function onSignedOut() {
  toggleContainers(false);
  popup.close();
  calendarIds = [];
  calendarData = [];
  events = [];
  readonlyCals = [];
  myResources = [];
  inst.setOptions({ resources: undefined, invalid: undefined, data: [] });
}

var inst = mobiscroll.eventcalendar('#demo-sync-events-outlook-calendar', {
  view: {
    timeline: {
      type: 'week',
      eventDisplay: 'fill',
    },
  },
  exclusiveEndDates: true,
  clickToCreate: false,
  dragToCreate: false,
  dragToMove: false,
  dragToResize: false,
  renderHeader: function () {
    return (
      '<div mbsc-calendar-nav class="md-sync-events-outlook-nav"></div>' +
      '<div class="md-spinner">' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '<div class="md-spinner-blade"></div>' +
      '</div>' +
      '<div class="md-outlook-calendar-buttons">' +
      '<button mbsc-button id="outlook-cal-sign-in" class="md-sync-events-outlook-button">Sync my outlook calendars</button>' +
      '<button mbsc-button id="outlook-cal-get-calendars" style="display:none" class="md-sync-events-outlook-button">My Calendars</button>' +
      '<button mbsc-button id="outlook-cal-today">Today</button></div>' +
      '<button mbsc-calendar-prev></button>' +
      '<button mbsc-calendar-next></button>' +
      '</div>'
    );
  },
  onPageLoading: function (args) {
    startDate = args.viewStart;
    endDate = args.viewEnd;
    clearTimeout(debounce);
    debounce = setTimeout(function () {
      if (outlookCalendarSync.isSignedIn()) {
        loadingEvents(true);
        outlookCalendarSync
          .getEvents(calendarIds, startDate, endDate)
          .then(function (resp) {
            loadingEvents(false);
            resp.forEach(function (event) {
              event.resource = event.outlookCalendarId;
            });
            events = resp;
            inst.setEvents(events);
          })
          .catch(onError);
      }
    }, 200);
  },
  onEventCreate: function (args) {
    if (outlookCalendarSync.isSignedIn()) {
      var event = args.event;
      var calendarId = event.resource;
      if (readonlyCals.indexOf(calendarId) !== -1) {
        mobiscroll.toast({
          message: 'This calendar is readonly',
        });
      } else {
        outlookCalendarSync
          .addEvent(calendarId, event)
          .then(function (resp) {
            inst.removeEvent(event);
            resp.resource = event.resource;
            inst.addEvent(resp);
            events.push(resp);
            mobiscroll.toast({
              message: 'Event created on "' + calendarData[calendarId].name + '" calendar',
            });
          })
          .catch(function (error) {
            inst.removeEvent(args.event);
            onError(error);
          });
      }
    }
  },
  onEventUpdate: function (args) {
    if (outlookCalendarSync.isSignedIn()) {
      mobiscroll
        .confirm({
          title: 'Are you sure you want to update this event?',
          message: 'This action will affect your outlook Calendar event.',
          okText: 'Update',
        })
        .then(function (result) {
          if (result) {
            var event = args.event;
            var calendarId = event.outlookCalendarId;
            outlookCalendarSync
              .updateEvent(calendarId, event)
              .then(function () {
                mobiscroll.toast({
                  message: 'Event updated on "' + calendarData[calendarId].name + '" calendar',
                });
              })
              .catch(function (error) {
                inst.updateEvent(args.oldEvent);
                onError(error);
              });
          } else {
            inst.updateEvent(args.oldEvent);
          }
        });
    }
  },
  onEventDelete: function (args) {
    if (outlookCalendarSync.isSignedIn()) {
      mobiscroll
        .confirm({
          title: 'Are you sure you want to delete this event?',
          message: 'This action will remove the event from your outlook Calendar as well.',
          okText: 'Delete',
        })
        .then(function (result) {
          if (result) {
            var event = args.event;
            var calendarId = event.outlookCalendarId;
            outlookCalendarSync
              .deleteEvent(calendarId, event)
              .then(function () {
                inst.removeEvent(event);
                events = events.filter(function (ev) {
                  return event.id !== ev.id;
                });
                mobiscroll.toast({
                  message: 'Event deleted from "' + calendarData[calendarId].name + '" calendar',
                });
              })
              .catch(onError);
          }
        });
    }
    return false;
  },
});

var popup = mobiscroll.popup('#demo-sync-events-outlook-calendar-popup', {
  anchor: document.getElementById('outlook-cal-get-calendars'),
  width: 400,
  touchUi: false,
  showOverlay: false,
  scrollLock: false,
  contentPadding: false,
  display: 'anchored',
});

// Sign in
calendarElm.addEventListener('click', function (ev) {
  if (ev.target.id === 'outlook-cal-sign-in') {
    if (!outlookCalendarSync.isSignedIn()) {
      outlookCalendarSync.signIn().catch(onError);
    }
  } else if (ev.target.id === 'outlook-cal-today') {
    inst.navigate(new Date());
  } else if (ev.target.id === 'outlook-cal-get-calendars') {
    popup.open();
  }
});

// Switch click
popupElm.addEventListener('change', function (ev) {
  if (ev.target.classList.contains('outlook-calendar-switch')) {
    loadEvents(ev.target.checked, ev.target.value);
  }
});

// Edit click
editButton.addEventListener('change', function (ev) {
  var isEditable = ev.target.checked;
  inst.setOptions({
    clickToCreate: isEditable,
    dragToCreate: isEditable,
    dragToMove: isEditable,
    dragToResize: isEditable,
  });
});

// Sign out
popupElm.addEventListener('click', function (ev) {
  if (ev.target.id === 'outlook-cal-sign-out') {
    outlookCalendarSync.signOut().catch(onError);
  }
});

// Init outlook client
outlookCalendarSync.init({
  clientId: '<YOUR_OUTLOOK_CLIENT_ID>',
  redirectUri: '<YOUR_OUTLOOK_REDIRECT_URI>',
  onSignedIn: onSignedIn,
  onSignedOut: onSignedOut,
});
```

### HTML

```html
<div id="demo-sync-events-outlook-calendar"></div>

<div id="demo-sync-events-outlook-calendar-popup">

    <div class="mbsc-form-group-inset mbsc-align-center">
        <p class="mbsc-italic mbsc-txt-muted">Editing events sync back to your calendar when enabled. You'll be asked for confirmation on every action.</p>
    </div>
    <div class="mbsc-form-group-inset">
        <label>
            <input id="demo-outlook-cal-edit" mbsc-switch data-label="Enable editing" />
        </label>
    </div>

    <div class="mbsc-form-group-inset md-sync-events-outlook-inset">
        <div class="mbsc-form-group-title">My calendars</div>
        <div id="outlook-cal-list"></div>
    </div>
    <div class="mbsc-form-group-inset">
        <button mbsc-button id="outlook-cal-sign-out" class="md-sync-events-outlook-button mbsc-button-block">Log out of my account</button>
    </div>

</div>
```

### CSS

```css
.md-outlook-calendar-buttons {
    flex: 1 0 auto;
    display: flex;
    justify-content: flex-end;
    margin-right: 10px;
}

.md-sync-events-outlook-calendar {
    border-left: 1px solid #ccc;
}

.md-outlook-calendar-header {
    flex: 1 0 auto;
    display: flex;
    justify-content: flex-end;
}

.md-sync-events-outlook-nav {
    justify-content: flex-start;
}

.md-sync-events-outlook-button.mbsc-button {
    text-transform: capitalize;
}

.md-sync-events-outlook-inset {
    margin-bottom: 0;
}

/* loading spinner and overlay */

.md-loading-events .md-sync-events-overlay {
    position: absolute;
    z-index: 2;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    -webkit-transform: translateZ(0);
    transform: translateZ(0);
}

.md-spinner {
    visibility: hidden;
    position: relative;
    width: 20px;
    height: 20px;
}

.md-loading-events .md-spinner {
    visibility: visible;
}

.md-spinner .md-spinner-blade {
    position: absolute;
    left: 44.5%;
    top: 37%;
    width: 10%;
    height: 25%;
    border-radius: 50%/20%;
    background-color: #8C8C8C;
    -webkit-animation: md-spinner-fade 1s linear infinite;
    animation: md-spinner-fade 1s linear infinite;
    -webkit-animation-play-state: paused;
    animation-play-state: paused;
}

.md-spinner .md-spinner-blade:nth-child(1) {
    -webkit-animation-delay: -1.66667s;
    animation-delay: -1.66667s;
    -webkit-transform: rotate(30deg) translate(0, -150%);
    transform: rotate(30deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(2) {
    -webkit-animation-delay: -1.58333s;
    animation-delay: -1.58333s;
    -webkit-transform: rotate(60deg) translate(0, -150%);
    transform: rotate(60deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(3) {
    -webkit-animation-delay: -1.5s;
    animation-delay: -1.5s;
    -webkit-transform: rotate(90deg) translate(0, -150%);
    transform: rotate(90deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(4) {
    -webkit-animation-delay: -1.41667s;
    animation-delay: -1.41667s;
    -webkit-transform: rotate(120deg) translate(0, -150%);
    transform: rotate(120deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(5) {
    -webkit-animation-delay: -1.33333s;
    animation-delay: -1.33333s;
    -webkit-transform: rotate(150deg) translate(0, -150%);
    transform: rotate(150deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(6) {
    -webkit-animation-delay: -1.25s;
    animation-delay: -1.25s;
    -webkit-transform: rotate(180deg) translate(0, -150%);
    transform: rotate(180deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(7) {
    -webkit-animation-delay: -1.16667s;
    animation-delay: -1.16667s;
    -webkit-transform: rotate(210deg) translate(0, -150%);
    transform: rotate(210deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(8) {
    -webkit-animation-delay: -1.08333s;
    animation-delay: -1.08333s;
    -webkit-transform: rotate(240deg) translate(0, -150%);
    transform: rotate(240deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(9) {
    -webkit-animation-delay: -1s;
    animation-delay: -1s;
    -webkit-transform: rotate(270deg) translate(0, -150%);
    transform: rotate(270deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(10) {
    -webkit-animation-delay: -0.91667s;
    animation-delay: -0.91667s;
    -webkit-transform: rotate(300deg) translate(0, -150%);
    transform: rotate(300deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(11) {
    -webkit-animation-delay: -0.83333s;
    animation-delay: -0.83333s;
    -webkit-transform: rotate(330deg) translate(0, -150%);
    transform: rotate(330deg) translate(0, -150%);
}

.md-spinner .md-spinner-blade:nth-child(12) {
    -webkit-animation-delay: -0.75s;
    animation-delay: -0.75s;
    -webkit-transform: rotate(360deg) translate(0, -150%);
    transform: rotate(360deg) translate(0, -150%);
}

.md-loading-events .md-spinner-blade {
    -webkit-animation-play-state: running;
    animation-play-state: running;
}

@-webkit-keyframes md-spinner-fade {
    0% {
        opacity: 0.85;
    }
    50% {
        opacity: 0.25;
    }
    100% {
        opacity: 0.25;
    }
}

@keyframes md-spinner-fade {
    0% {
        opacity: 0.85;
    }
    50% {
        opacity: 0.25;
    }
    100% {
        opacity: 0.25;
    }
}
```

## Implementation notes

- Use `timeline: { type: 'week', eventDisplay: 'fill' }` - a weekly view where events fill the width of their cell.
- No initial resources or events - both are loaded dynamically from the user's Outlook account after sign-in.
- Set `exclusiveEndDates: true` on the calendar.
- Import `outlookCalendarSync` from `@mobiscroll/calendar-integration` (a separately installed package).
- Call `outlookCalendarSync.init()` on component mount with:
  - `clientId` - your Azure app client ID
  - `redirectUri` - your OAuth redirect URI
  - `onSignedIn` callback - triggered when the user completes OAuth
  - `onSignedOut` callback - triggered when the user signs out
- **`onSignedIn`**:
  1. Call `outlookCalendarSync.getCalendars()` to retrieve the user's calendar list.
  2. Sort calendars so the default calendar appears first (`c.isDefaultCalendar ? -1 : 1`).
  3. Build resources: `{ id: c.id, name: c.name, color: c.hexColor }` - each calendar becomes a timeline row.
  4. Build a `readonlyCalendars` list: calendars where `!c.canEdit`.
  5. Set an `invalid` block to grey out all time slots on readonly rows: `[{ recurring: { repeat: 'daily', interval: 1 }, resource: readonlyCalendarIds }]`.
  6. Call `outlookCalendarSync.getEvents(calendarIds, startDate, endDate)` and map `event.resource = event.outlookCalendarId` on each result.
- **`onSignedOut`**: Clear all events, resources, calendarIds, and calendarData. Close the popup if open.
- **`onPageLoading`**: Fires on every page navigation. Debounce by 200ms (clear the previous timeout, set a new one). Inside the timeout, if `outlookCalendarSync.isSignedIn()`, call `outlookCalendarSync.getEvents(calendarIds, args.viewStart, args.viewEnd)` and replace the events array. Store `args.viewStart` / `args.viewEnd` in variables - they are also needed by the toggle-calendar handler.

  > **Vue:** Uses `@page-loading` event binding; all others use `onPageLoading`.

- **Toggle calendar**: To enable a calendar, push its ID into `calendarIds`, call `getEvents([calendarId], startDate, endDate)`, add the new resource, and append the returned events. To disable, filter the resource, calendarId, and events out by `event.outlookCalendarId !== calendarId`.
- All four drag/create/move/resize options (`clickToCreate`, `dragToCreate`, `dragToMove`, `dragToResize`) are bound to the "Enable editing" Switch toggle - off by default, toggled by the user in the popup.
- **`onEventCreate`**:
  1. Check if `event.resource` is in `readonlyCalendars`. If so, show a "This calendar is readonly" toast and return.
  2. Otherwise, call `outlookCalendarSync.addEvent(calendarId, event)`. On success, replace the optimistic event with the returned Outlook event (`newEvent.resource = event.resource`). On failure, remove the optimistic event and show an error toast.
- **`onEventUpdate`**:
  1. Show a confirmation dialog: "Are you sure you want to update this event?" / "This action will affect your Outlook Calendar event." / OK: "Update".
  2. If confirmed, call `outlookCalendarSync.updateEvent(event.outlookCalendarId, event)`.
  3. If cancelled or API call fails, restore `args.oldEvent`.
- **`onEventDelete`**:
  1. Return `false` to suppress default delete behavior.
  2. Show a confirmation dialog: "Are you sure you want to delete this event?" / "This action will remove the event from your Outlook Calendar as well." / OK: "Delete".
  3. If confirmed, call `outlookCalendarSync.deleteEvent(event.outlookCalendarId, event)` and filter the event out on success.

  > **Angular:** Wrap all async Outlook API `.then()` callbacks (Create, Update, Delete) in `NgZone.run()` to ensure change detection fires after async operations.

  For the imperative API, use `inst.updateEvent()`, `inst.removeEvent()`, and `inst.addEvent()` to mutate the calendar directly instead of updating the events array.

- **Custom header**: Replace the default header with `CalendarNav` (left-aligned), a loading spinner (visible while events load), and a button group (right-aligned): a "Sync my outlook calendars" button when not signed in, a "My Calendars" button when signed in, a "Today" button, and `CalendarPrev` / `CalendarNext`.
- **Popup**: An anchored popup on the "My Calendars" button. Contains a note about sync behavior, an "Enable editing" Switch (controls all four drag/create options), a "My Calendars" section with one Switch per calendar (each toggles visibility and events), and a "Log out of my account" button → calls `outlookCalendarSync.signOut()`.

## Run it in your project

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: [Google Calendar Sync](https://demo.mobiscroll.com/javascript/timeline/sync-events-google-calendar.md) - Load Google Calendar events onto a Javascript Timeline — authenticate users, display calendars as resource rows, and sync changes back to Google Calendar.
- Next: [Load Events from Google Calendar](https://demo.mobiscroll.com/javascript/timeline/load-events-from-google-calendar.md) - Load public Google Calendar events into a Javascript Timeline — show holiday calendars as resource rows using a Calendar ID and API key.
