<!DOCTYPE html>
<html>

<head>

  <!-- css for the calendar -->
  <link data-require="bootstrap-css@3.0.2" data-semver="3.0.2" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css" />
  <link data-require="fullcalendar@1.6.1" data-semver="1.6.1" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.1/fullcalendar.min.css" />

  <!-- jquery & angular frameworks (must come in this order!) -->
  <script data-require="jquery@*" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
  <script data-require="angular.js@1.2.2" data-semver="1.2.2" src="http://code.angularjs.org/1.2.2/angular.js"></script>

  <!-- fullCalendar plus the anguluar adapter -->
  <script data-require="fullcalendar@1.6.1" data-semver="1.6.1" src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.1/fullcalendar.min.js"></script>
  <script src="angular-ui-calendar.js"></script>

  <!-- Google API client library and helper -->
  <script src="https://apis.google.com/js/client.js"></script>
  <script src="gapi-helper.js"></script>

</head>

<body ng-app="CalendarApp">
  <div ng-controller="CalendarController">

    <!--Add a button for the user to click to initiate auth sequence -->
    <button id="authorize-button" ng-show="authNeeded" ng-click="requestAuth()">Authorize</button>
    <button id="logout-button" ng-hide="authNeeded" onclick="window.open('https://accounts.google.com/logout')">Sign Out</button>
    <div class="calendar" ng-model="eventSources" calendar="calendar" config="uiConfig.calendar" ui-calendar="uiConfig.calendar"></div>
  
  </div>

  <!-- the app -->
  <script src="app.js"></script>
  <script src="factories.js"></script>
  <script src="controller.js"></script>

</body>

</html>
/**
 * https://github.com/dr-skot/gapi-helper
 * 
 */
gapi_helper = {
  start: new Date(),
  listeners: {},
  status: {}
};

// for logging elapsed times to console
gapi_helper.time = function() {
  return new Date() - gapi_helper.start;
};

// config must contain clientId, apiKey, scopes, and services to load, eg
// { clientId: "<id>", apiKey: "<key>", scopes: "https://www.googleapis.com/auth/calendar", 
//   services: { calendar: 'v3' } }
gapi_helper.configure = function(config) {
  console.log("gapi configured %s", gapi_helper.time());
  // TODO: confirm valid config
  gapi_helper.config = config;
  if (gapi_helper.status.scriptLoaded) gapi_helper.init();
};

gapi_helper.onScriptLoad = function() {
  console.log("gapi script loaded %s", gapi_helper.time());
  gapi_helper.status.scriptLoaded = true;
  if (gapi_helper.config) gapi_helper.init();
};
// this synonym is needed by the '?onload=' construction, which seems to choke on object notatation
// use «script src="https://apis.google.com/js/client.js?onload=gapi_helper_onScriptLoad»
gapi_helper_onScriptLoad = gapi_helper.onScriptLoad;

gapi_helper.init = function() {
  console.log("gapi_helper.init %s", gapi_helper.time());
  gapi.client.setApiKey(gapi_helper.config.apiKey);
  window.setTimeout(gapi_helper.checkAuth, 1);
};

gapi_helper.checkAuth = function() {
  console.log("gapi_helper.checkAuth %s", gapi_helper.time());
  gapi.auth.authorize({
    client_id: gapi_helper.config.clientId, 
    scope: gapi_helper.config.scopes, 
    immediate: true
  }, gapi_helper.handleAuthResult);
};

gapi_helper.handleAuthResult = function(authResult) {
  console.log("gapi_helper.handleAuthResult %s", gapi_helper.time());
  if (authResult && !authResult.error) {
    gapi_helper.fireEvent('authorized');
    gapi_helper.loadServices();
  } else {
    gapi_helper.fireEvent('authFailed');
  }
};
  
gapi_helper.requestAuth = function(event) {
  console.log("gapi_helper.requestAuth %s", gapi_helper.time());
  gapi.auth.authorize({
    client_id: gapi_helper.config.clientId, 
    scope: gapi_helper.config.scopes, 
    immediate: false,
    cookie_policy: "single_host_origin"
  }, gapi_helper.handleAuthResult);
  return false; // so you can use this as an onclick handler
};

gapi_helper.loadServices = function() {
  console.log("gapi_helper.loadServices %s", gapi_helper.time());
    
  function eventFirer(name, version) {
    return function(result) {
        console.log("%s %s loaded %s %O", name, version, gapi_helper.time(), result);
        gapi_helper.fireEvent(name + 'Loaded');
    };
  }
  
  for (var name in gapi_helper.config.services) {
    var version = gapi_helper.config.services[name];
    gapi.client.load(name, version, eventFirer(name, version));
  }
  
};

// TODO add gapi_helper.logout

gapi_helper.when = function(eventName, callback) {
  // if event has already happened, trigger the callback immediately
  if (gapi_helper.status[eventName]) callback();
  // in any case, add the callback to the listeners array
  if (!gapi_helper.listeners[eventName]) gapi_helper.listeners[eventName] = [];
  gapi_helper.listeners[eventName].push(callback);
  console.log('gapi_helper: registered listener for %s', eventName);
};

gapi_helper.fireEvent = function(eventName) {
  console.log("firing %s", eventName);
  // register event
  gapi_helper.status[eventName] = true;
  // trigger listeners
  var listeners = gapi_helper.listeners[eventName] || [];
  for (var i = 0; i < listeners.length; i++) {
    listeners[i]();
  }
};

gapi_helper.watcher = setInterval(function() {
    var loaded = typeof gapi !== "undefined" && gapi.client;
    console.log("%s %s", loaded ? "gapi loaded" : "waiting", gapi_helper.time());
    if (loaded) {
        clearTimeout(gapi_helper.watcher);
        gapi_helper.onScriptLoad();
    }
}, 500);

app.controller("CalendarController", function($scope, $location, CalendarData, EventSourceFactory) {
  $scope.eventSources = [];
  $scope.authNeeded = false;

  // load calendars from google and pass them as event sources to fullcalendar
  $scope.loadSources = function() {
    EventSourceFactory.getEventSources().then(function(result) {
      $scope.$log.debug("event sources %O", result);
      $scope.eventSources = result;
      angular.forEach(result, function(source) {
        $scope.calendar.fullCalendar('addEventSource', source);
      });
    });
  };

  // request Google authorization from the user
  $scope.requestAuth = function() {
    gapi_helper.requestAuth();
  };
  
  // configure gapi-helper
  // (you'll have to change these values for your own app)
  gapi_helper.configure({
    clientId: '721457523650-pjoaci65s9ob0241fbb151i3u7sjvv3r.apps.googleusercontent.com',
    apiKey: 'AIzaSyDY4uF058d78YHd7SPaF8bH0aoJqPGKXFU',
    scopes: 'https://www.googleapis.com/auth/calendar',
    services: {
      calendar: 'v3'
    }
  });

  // set authNeeded to appropriate value on auth events
  gapi_helper.when('authorized', function() {
    $scope.$apply(function() {
      $scope.authNeeded = false;
    });
  });
  gapi_helper.when('authFailed', function() {
    $scope.$apply(function() {
      $scope.authNeeded = true;
    });
  });

  // load the event sources when the calendar api is loaded
  gapi_helper.when('calendarLoaded', $scope.loadSources);

});
app = angular.module('CalendarApp', ['ui.calendar']);

app.run(function($rootScope, $log){
  $rootScope.$log = $log;
});
app.factory('CalendarData', function($q, $log) {
  var self = {};

  // gets the calendar list from Google
  self.getCalendars = function() {
    var deferred = $q.defer();
    var request = gapi.client.calendar.calendarList.list();
    request.execute(function(resp) {
      $log.debug("CalendarData.getCalendars response %O", resp);
      deferred.resolve(resp.items);
    });
    return deferred.promise;
  };

  // fetches events from a particular calendar
  // start and end dates (optional) must be RFC 3339 format 
  self.getEvents = function(calendarId, start, end) {
    var deferred = $q.defer();
    if (gapi_helper.status.calendarLoaded) {
      var request = gapi.client.calendar.events.list({
        calendarId: calendarId,
        timeMin: start,
        timeMax: end,
        maxResults: 10000, // max results causes problems: http://goo.gl/FqwIFh
        singleEvents: true
      });
      request.execute(function(resp) {
        $log.debug("CalendarData.getEvents response %O", resp);
        deferred.resolve(resp.items || []);
      });
    } else {
      deferred.reject([]);
    }
    return deferred.promise;
  };

  return self;
});

app.factory("EventSourceFactory", function($q, CalendarData) {
  var self = {};

  self.eventCache = {};
  // if cached data is older than this, don't display it; wait for server data
  self.displayableTime = 1000 * 60 * 5; // 5 minutes
  // if cached data is younger than this, don't bother hitting the server at all
  self.noUpdateTime = 1000 * 30; // 30 seconds
  // (if age falls inbetween, display cached, then query server in the bg to update cache)

  // converts unix timestamp to Google API date format (RFC 3339)
  self.ts2googleDate = function(ts) {
    return $.fullCalendar.formatDate($.fullCalendar.parseDate(ts), 'u');
  };

  // reformats events from Google's API into an object fullcalendar can use
  self.google2fcEvent = function(google) {
    var fc = {
      title: google.summary,
      start: google.start.date || google.start.dateTime,
      end: google.end.date || google.end.dateTime,
      allDay: google.start.date ? true : false,
      google: google // keep a reference to the original
    };
    if (fc.allDay) {
      // subtract 1 from end date: Google all-day end dates are exclusive
      // FullCalendar's are inclusive
      var end = $.fullCalendar.parseDate(fc.end);
      end.setDate(end.getDate() - 1);
      fc.end = $.fullCalendar.formatDate(end, 'yyyy-MM-dd');
    }
    return fc;
  };

  // fetches events from Google
  // callback is called with the results when they arrive
  self.fetchEvents = function(calendarId, start, end, callback) {
    start = self.ts2googleDate(start);
    end = self.ts2googleDate(end);
    CalendarData.getEvents(calendarId, start, end).then(function(result) {
      callback(result.map(self.google2fcEvent));
    });
  };

  // gets events, possibly from the cache if it's not stale
  self.getEvents = function(calendarId, start, end, callback) {
    var key = calendarId + '|' + start + '|' + end;
    var cached = self.eventCache[key];
    var now = new Date();
    var displayCached = false,
      updateCache = true;
    if (cached) {
      var age = new Date().getTime() - cached.date.getTime();
      displayCached = age <= self.displayableTime;
      updateCache = age > self.noUpdateTime;
    }
    // cached data is ok to display? then display it
    if (displayCached) {
      callback(cached.data);
    }
    // do we need to update the cache with fresh data from Google?
    if (updateCache) {
      self.fetchEvents(calendarId, start, end, function(data) {
        self.eventCache[key] = {
          date: new Date(),
          data: data
        };
        // display the fresh data if we didn't display the cached data
        if (!displayCached) callback(data);
      });
    }
  };

  // converts a calendar object from Google's API to a fullcalendar event source
  self.google2fcEventSource = function(calendar) {
    return {
      events: function(start, end, callback) {
        self.getEvents(calendar.id, start, end, callback);
      },
      textColor: calendar.foregroundColor,
      color: calendar.backgroundColor,
      google: calendar // keep a reference to the original
    };
  };

  // gets event sources for all calendars in the user's Google account
  self.getEventSources = function() {
    var deferred = $q.defer();
    CalendarData.getCalendars().then(function(result) {
      sources = result.map(self.google2fcEventSource);
      deferred.resolve(sources);
    }, function(error) {
      $scope.$log("EventSourceFactory.getEventSources error %O", error);
      deferred.reject(error);
    });
    return deferred.promise;
  };

  return self;

});
/*
*  AngularJs Fullcalendar Wrapper for the JQuery FullCalendar
*  API @ http://arshaw.com/fullcalendar/
*
*  Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes.
*       Can also take in multiple event urls as a source object(s) and feed the events per view.
*       The calendar will watch any eventSource array and update itself when a change is made.
*
*/

angular.module('ui.calendar', [])
  .constant('uiCalendarConfig', {})
  .directive('uiCalendar', ['uiCalendarConfig', '$parse', function(uiCalendarConfig) {
  uiCalendarConfig = uiCalendarConfig || {};
  var sourceSerialId = 1, eventSerialId = 1;
  //returns calendar
  return {
    require: 'ngModel',
    scope: {ngModel:'=',config:'='},
    restrict: 'A',
    link: function(scope, elm, attrs) {
      var sources = scope.ngModel;
      scope.destroy = function(){
        if(attrs.calendar){
          scope.calendar = scope.$parent[attrs.calendar] =  elm.html('');
        }else{
          scope.calendar = elm.html('');
        }
      };
      scope.destroy();
      scope.init = function(){
        var options = { eventSources: sources };
        angular.extend(options, uiCalendarConfig, attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {});
        scope.calendar.fullCalendar(options);
      };
      scope.init();

      // Track changes in array by assigning id tokens to each element and watching the scope for changes in those tokens
      // arguments:
      //  arraySource array of function that returns array of objects to watch
      //  tokenFn function(object) that returns the token for a given object
      var changeWatcher = function(arraySource, tokenFn) {
        var self;
        var getTokens = function() {
          var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
          return array.map(function(el) {
            var token = tokenFn(el);
            map[token] = el;
            return token;
          });
        };
        // returns elements in that are in a but not in b
        // subtractAsSets([4, 5, 6], [4, 5, 7]) => [6]
        var subtractAsSets = function(a, b) {
          var result = [], inB = {}, i, n;
          for (i = 0, n = b.length; i < n; i++) {
            inB[b[i]] = true;
          }
          for (i = 0, n = a.length; i < n; i++) {
            if (!inB[a[i]]) {
              result.push(a[i]);
            }
          }
          return result;
        };

        // Map objects to tokens and vice-versa
        var map = {};

        var applyChanges = function(newTokens, oldTokens) {
          var i, n, el, token;
          var replacedTokens = {};
          var removedTokens = subtractAsSets(oldTokens, newTokens);
          for (i = 0, n = removedTokens.length; i < n; i++) {
            var removedToken = removedTokens[i];
            el = map[removedToken];
            delete map[removedToken];
            var newToken = tokenFn(el);
            // if the element wasn't removed but simply got a new token, its old token will be different from the current one
            if (newToken === removedToken) {
              self.onRemoved(el);
            } else {
              replacedTokens[newToken] = removedToken;
              self.onChanged(el);
            }
          }
          var addedTokens = subtractAsSets(newTokens, oldTokens);
          for (i = 0, n = addedTokens.length; i < n; i++) {
            token = addedTokens[i];
            el = map[token];
            if (!replacedTokens[token]) {
              self.onAdded(el);
            }
          }
        };
        return self = {
          subscribe: function(scope) {
            scope.$watch(getTokens, applyChanges, true);
          },
          onAdded: angular.noop,
          onChanged: angular.noop,
          onRemoved: angular.noop
        };
      };

      //= tracking sources added/removed

      var eventSourcesWatcher = changeWatcher(sources, function(source) {
        return source.__id || (source.__id = sourceSerialId++);
      });
      eventSourcesWatcher.subscribe(scope);
      eventSourcesWatcher.onAdded = function(source) {
        scope.calendar.fullCalendar('addEventSource', source);
      };
      eventSourcesWatcher.onRemoved = function(source) {
        scope.calendar.fullCalendar('removeEventSource', source);
      };

      //= tracking individual events added/changed/removed
      var allEvents = function() {
        // return sources.flatten(); but we don't have flatten
        var arraySources = [];
        for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
          var source = sources[i];
          if (angular.isArray(source)) {
            arraySources.push(source);
          }
        }
        return Array.prototype.concat.apply([], arraySources);
      };
      var eventsWatcher = changeWatcher(allEvents, function(e) {
        if (!e.__uiCalId) {
          e.__uiCalId = eventSerialId++;
        }
        // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
        return "" + e.__uiCalId + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
            (e.allDay || false) + (e.className || '');
      });
      eventsWatcher.subscribe(scope);
      eventsWatcher.onAdded = function(event) {
        scope.calendar.fullCalendar('renderEvent', event);
      };
      eventsWatcher.onRemoved = function(event) {
        scope.calendar.fullCalendar('removeEvents', function(e) { return e === event; });
      };
      eventsWatcher.onChanged = function(event) {
        scope.calendar.fullCalendar('updateEvent', event);
      };
    }
  };
}]);