---
title: "JQuery Timeline Google Calendar Sync Example | Mobiscroll"
component: "Timeline"
framework: "jQuery"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/jquery/timeline/sync-events-google-calendar"
---

# Google Calendar Sync - jQuery Timeline demo

Load Google Calendar events onto a JQuery Timeline — authenticate users, display calendars as resource rows, and sync changes back to Google Calendar.

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

## Description

The event calendar comes with third party calendar integration support through the [**integration plugin**](https://mobiscroll.com/docs/jquery/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 Google 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/jquery/eventcalendar/calendar-integrations) to connect to and load events from Google Calendar 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 Google 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 Google Calendars` button in the header prompts the user to sign in to Google Calendar.
- **Authentication flow** Clicking the Google sign-in button opens a new browser tab where the user can authenticate with their Google 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 Google 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 Google 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 Google Calendar.
- **Sync feedback** Creating, updating, or deleting an event shows a bottom-centered toast confirming the change in the selected Google 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 Google 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, Google 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 Google Calendar.
- **Google Calendar sync workflows** Creating scheduling interfaces where event changes need to sync back to Google Calendar.
- **Multi-calendar timelines** Showing several Google calendars, such as personal, shared, and holiday calendars, as separate timeline rows.
- **Calendar visibility controls** Letting users toggle individual Google 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/jquery/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-themeVariant) - option
- [`setOptions`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-setOptions) - method
- [`resources`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-resources) - option
- [`data`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-data) - option
- [`invalid`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-invalid) - option
- [`setEvents`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-setEvents) - method
- [`close`](https://mobiscroll.com/docs/jquery/popup/api#method-close) - method
- [`view`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-view) - option
- [`exclusiveEndDates`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-exclusiveEndDates) - option
- [`clickToCreate`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/jquery/eventcalendar/api#opt-dragToResize) - option
- [`renderHeader`](https://mobiscroll.com/docs/jquery/eventcalendar/api#renderer-renderHeader) - renderer
- [`onPageLoading`](https://mobiscroll.com/docs/jquery/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreate`](https://mobiscroll.com/docs/jquery/eventcalendar/api#event-onEventCreate) - event
- [`removeEvent`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-removeEvent) - method
- [`addEvent`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-addEvent) - method
- [`onEventUpdate`](https://mobiscroll.com/docs/jquery/eventcalendar/api#event-onEventUpdate) - event
- [`updateEvent`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-updateEvent) - method
- [`onEventDelete`](https://mobiscroll.com/docs/jquery/eventcalendar/api#event-onEventDelete) - event
- [`anchor`](https://mobiscroll.com/docs/jquery/popup/api#opt-anchor) - option
- [`width`](https://mobiscroll.com/docs/jquery/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/jquery/popup/api#opt-touchUi) - option
- [`showOverlay`](https://mobiscroll.com/docs/jquery/popup/api#opt-showOverlay) - option
- [`scrollLock`](https://mobiscroll.com/docs/jquery/popup/api#opt-scrollLock) - option
- [`display`](https://mobiscroll.com/docs/jquery/popup/api#opt-display) - option
- [`navigate`](https://mobiscroll.com/docs/jquery/eventcalendar/api#method-navigate) - method
- [`open`](https://mobiscroll.com/docs/jquery/popup/api#method-open) - method

## Code

### JS

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

$(function () {
  var googleCalendarSync = mobiscroll.googleCalendarSync;

  var $calendar = $('#demo-sync-events-google-calendar');
  var $popup = $('#demo-sync-events-google-calendar-popup');
  var $editButton = $('#demo-google-cal-edit');

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

  function toggleContainers(loggedIn) {
    var $loggedOutCont = $('#google-cal-sign-in');
    var $loggedInCont = $('#google-cal-get-calendars');

    if (loggedIn) {
      $loggedOutCont.hide();
      $loggedInCont.show();
    } else {
      $loggedInCont.hide();
      $loggedOutCont.show();
    }
  }

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

  function loadingEvents(show) {
    if (show) {
      $calendar.addClass('md-loading-events');
    } else {
      $calendar.removeClass('md-loading-events');
    }
  }

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

  function onSignedIn() {
    toggleContainers(true);
    googleCalendarSync
      .getCalendars()
      .then(function (calendars) {
        var calList = '';

        calendars.sort(function (c) {
          return c.primary ? -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.summary +
            '" type="checkbox" class="google-calendar-switch" checked />' +
            '</label>';

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

          if (!(c.accessRole === 'writer' || c.accessRole === 'owner')) {
            readonlyCals.push(c.id);
          }
        }

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

        var $calendarList = $('#google-cal-list');
        $calendarList.append(calList);

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

        mobiscroll.enhance($calendarList[0]);
      })
      .catch(onError);
  }

  function onSignedOut() {
    toggleContainers(false);
    popup.close();
    calendarIds = [];
    calendarData = {};
    events = [];
    readonlyCals = [];
    myResources = [];
    inst.setOptions({ resources: undefined, invalid: undefined, data: [] });
    $('#google-cal-list').empty();
  }

  var inst = $calendar
    .mobiscroll()
    .eventcalendar({
      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-google-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-google-calendar-buttons">' +
          '<button mbsc-button id="google-cal-sign-in" class="md-sync-events-google-button">Sync my google calendars</button>' +
          '<button mbsc-button id="google-cal-get-calendars" style="display:none" class="md-sync-events-google-button">My Calendars</button>' +
          '<button mbsc-button id="google-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 (googleCalendarSync.isSignedIn()) {
            loadingEvents(true);
            googleCalendarSync
              .getEvents(calendarIds, startDate, endDate)
              .then(function (resp) {
                loadingEvents(false);
                resp.forEach(function (event) {
                  event.resource = event.googleCalendarId;
                });
                events = resp;
                inst.setEvents(events);
              })
              .catch(onError);
          }
        }, 200);
      },
      onEventCreate: function (args) {
        if (googleCalendarSync.isSignedIn()) {
          var event = args.event;
          var calendarId = event.resource;
          if (readonlyCals.indexOf(calendarId) !== -1) {
            mobiscroll.toast({
              message: 'This calendar is readonly',
            });
          } else {
            googleCalendarSync
              .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 (googleCalendarSync.isSignedIn()) {
          mobiscroll
            .confirm({
              title: 'Are you sure you want to update this event?',
              message: 'This action will affect your Google Calendar event.',
              okText: 'Update',
            })
            .then(function (result) {
              if (result) {
                var event = args.event;
                var calendarId = event.googleCalendarId;
                googleCalendarSync
                  .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 (googleCalendarSync.isSignedIn()) {
          mobiscroll
            .confirm({
              title: 'Are you sure you want to delete this event?',
              message: 'This action will remove the event from your Google Calendar as well.',
              okText: 'Delete',
            })
            .then(function (result) {
              if (result) {
                var event = args.event;
                var calendarId = event.googleCalendarId;
                googleCalendarSync
                  .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;
      },
    })
    .mobiscroll('getInst');

  var popup = $popup
    .mobiscroll()
    .popup({
      anchor: $('#google-cal-get-calendars')[0],
      width: 400,
      touchUi: false,
      showOverlay: false,
      scrollLock: false,
      contentPadding: false,
      display: 'anchored',
    })
    .mobiscroll('getInst');

  // Sign in
  $calendar.on('click', '#google-cal-sign-in', function () {
    if (!googleCalendarSync.isSignedIn()) {
      googleCalendarSync.signIn().catch(onError);
    }
  });

  $calendar.on('click', '#google-cal-today', function () {
    inst.navigate(new Date());
  });

  $calendar.on('click', '#google-cal-get-calendars', function () {
    popup.open();
  });

  // Switch click
  $popup.on('change', '.google-calendar-switch', function (ev) {
    loadEvents(ev.target.checked, ev.target.value);
  });

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

  $popup.on('click', '#google-cal-sign-out', function () {
    googleCalendarSync.signOut().catch(onError);
  });

  // Init google client
  googleCalendarSync.init({
    apiKey: '<YOUR_GOOGLE_API_KEY>',
    clientId: '<YOUR_GOOGLE_CLIENT_ID>',
    onSignedIn: onSignedIn,
    onSignedOut: onSignedOut,
  });
});
```

### HTML

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

<div id="demo-sync-events-google-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-google-cal-edit" mbsc-switch data-label="Enable editing" />
        </label>
    </div>

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

</div>
```

### CSS

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

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

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

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

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

.md-sync-events-google-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 Google account after sign-in.
- Set `exclusiveEndDates: true` on the calendar.
- Import `googleCalendarSync` from `@mobiscroll/calendar-integration` (a separately installed package).
- Call `googleCalendarSync.init()` on component mount with:
  - `apiKey` - your Google API key
  - `clientId` - your Google OAuth client ID
  - `onSignedIn` callback - triggered when the user completes OAuth
  - `onSignedOut` callback - triggered when the user signs out
- **`onSignedIn`**:
  1. Call `googleCalendarSync.getCalendars()` to retrieve the user's calendar list.
  2. Sort calendars so the primary calendar appears first (`c.primary ? -1 : 1`).
  3. Build resources: `{ id: c.id, name: c.summary, color: c.backgroundColor }` - each calendar becomes a timeline row.
  4. Build a `readonlyCalendars` list: calendars where `c.accessRole` is neither `'writer'` nor `'owner'`.
  5. Set an `invalid` block to grey out all time slots on readonly rows: `[{ recurring: { repeat: 'daily', interval: 1 }, resource: readonlyCalendarIds }]`.
  6. Call `googleCalendarSync.getEvents(calendarIds, startDate, endDate)` and map `event.resource = event.googleCalendarId` on each result.

  > **Angular:** Uses `c.hexColor` instead of `c.backgroundColor` for the resource color (step 3). Wrap all Google API `.then()` callbacks in `NgZone.run()` to ensure change detection fires after async operations.

- **`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 `googleCalendarSync.isSignedIn()`, call `googleCalendarSync.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.googleCalendarId !== 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 `googleCalendarSync.addEvent(calendarId, event)`. On success, replace the optimistic event with the returned Google 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 Google Calendar event." / OK: "Update".
  2. If confirmed, call `googleCalendarSync.updateEvent(calendarId, 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 Google Calendar as well." / OK: "Delete".
  3. If confirmed, call `googleCalendarSync.deleteEvent(calendarId, event)` and filter the event out on success.

  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 google 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 `googleCalendarSync.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/jquery/getting-started/installation

## Related demos

- Next: [Outlook Calendar Sync](https://demo.mobiscroll.com/jquery/timeline/sync-events-outlook-calendar.md) - Display Outlook Calendar events on a JQuery Timeline — each calendar as a separate resource row.
