text: /*
  *  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', {
          calendars : {}
      })
      .controller('uiCalendarCtrl', ['$scope', '$locale',
          function ($scope, $locale) {
  
              var sources = $scope.eventSources;
              var extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop;
  
              var wrapFunctionWithScopeApply = function (functionToWrap) {
                  return function () {
                      // This may happen outside of angular context, so create one if outside.
                      if ($scope.$root.$$phase) {
                          return functionToWrap.apply(this, arguments);
                      }
  
                      var args = arguments;
                      var that = this;
                      return $scope.$root.$apply(
                          function () {
                              return functionToWrap.apply(that, args);
                          }
                      );
                  };
              };
  
              var eventSerialId = 1;
              // @return {String} fingerprint of the event object and its properties
              this.eventFingerprint = function (e) {
                  if (!e._id) {
                      e._id = eventSerialId++;
                  }
  
                  var extraSignature = extraEventSignature({
                      event : e
                  }) || '';
                  var start = moment.isMoment(e.start) ? e.start.unix() : (e.start ? moment(e.start).unix() : '');
                  var end = moment.isMoment(e.end) ? e.end.unix() : (e.end ? moment(e.end).unix() : '');
  
                  // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
                  return [e._id, e.id || '', e.title || '', e.url || '', start, end, e.allDay || '', e.className || '', extraSignature].join('');
              };
  
              var sourceSerialId = 1;
              var sourceEventsSerialId = 1;
              // @return {String} fingerprint of the source object and its events array
              this.sourceFingerprint = function (source) {
                  var fp = '' + (source.__id || (source.__id = sourceSerialId++));
                  var events = angular.isObject(source) && source.events;
  
                  if (events) {
                      fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++));
                  }
                  return fp;
              };
  
              // @return {Array} all events from all sources
              this.allEvents = function () {
                  return Array.prototype.concat.apply(
                      [],
                      (sources || []).reduce(
                          function (previous, source) {
                              if (angular.isArray(source)) {
                                  previous.push(source);
                              } else if (angular.isObject(source) && angular.isArray(source.events)) {
                                  var extEvent = Object.keys(source).filter(
                                      function (key) {
                                          return (key !== '_id' && key !== 'events');
                                      }
                                  );
  
                                  source.events.forEach(
                                      function (event) {
                                          angular.extend(event, extEvent);
                                      }
                                  );
  
                                  previous.push(source.events);
                              }
                              return previous;
                          },
                          []
                      )
                  );
              };
  
              // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens
              // @param {Array|Function} arraySource array of objects to watch
              // @param tokenFn {Function} that returns the token for a given object
              // @return {Object}
              //  subscribe: function(scope, function(newTokens, oldTokens))
              //    called when source has changed. return false to prevent individual callbacks from firing
              //  onAdded/Removed/Changed:
              //    when set to a callback, called each item where a respective change is detected
              this.changeWatcher = function (arraySource, tokenFn) {
                  var self;
  
                  var getTokens = function () {
                      return ((angular.isFunction(arraySource) ? arraySource() : arraySource) || []).reduce(
                          function (rslt, el) {
                              var token = tokenFn(el);
                              map[token] = el;
                              rslt.push(token);
                              return rslt;
                          },
                          []
                      );
                  };
  
                  // @param {Array} a
                  // @param {Array} b
                  // @return {Array} elements in that are in a but not in b
                  // @example
                  //  subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100]
                  var subtractAsSets = function (a, b) {
                      var obj = (b || []).reduce(
                          function (rslt, val) {
                              rslt[val] = true;
                              return rslt;
                          },
                          Object.create(null)
                      );
                      return (a || []).filter(
                          function (val) {
                              return !obj[val];
                          }
                      );
                  };
  
                  // Map objects to tokens and vice-versa
                  var map = {};
  
                  // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
                  var applyChanges = function (newTokens, oldTokens) {
                      var i;
                      var token;
                      var replacedTokens = {};
                      var removedTokens = subtractAsSets(oldTokens, newTokens);
                      for (i = 0; i < removedTokens.length; i++) {
                          var removedToken = removedTokens[i];
                          var 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; i < addedTokens.length; i++) {
                          token = addedTokens[i];
                          if (!replacedTokens[token]) {
                              self.onAdded(map[token]);
                          }
                      }
                  };
  
                  self = {
                      subscribe : function (scope, onArrayChanged) {
                          scope.$watch(getTokens, function (newTokens, oldTokens) {
                              var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false);
                              if (notify) {
                                  applyChanges(newTokens, oldTokens);
                              }
                          }, true);
                      },
                      onAdded : angular.noop,
                      onChanged : angular.noop,
                      onRemoved : angular.noop
                  };
                  return self;
              };
  
              this.getFullCalendarConfig = function (calendarSettings, uiCalendarConfig) {
                  var config = {};
  
                  angular.extend(config, uiCalendarConfig);
                  angular.extend(config, calendarSettings);
  
                  angular.forEach(config, function (value, key) {
                      if (typeof value === 'function') {
                          config[key] = wrapFunctionWithScopeApply(config[key]);
                      }
                  });
  
                  return config;
              };
  
              this.getLocaleConfig = function (fullCalendarConfig) {
                  if (!fullCalendarConfig.lang && !fullCalendarConfig.locale || fullCalendarConfig.useNgLocale) {
                      // Configure to use locale names by default
                      var tValues = function (data) {
                          // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
                          return (Object.keys(data) || []).reduce(
                              function (rslt, el) {
                                  rslt.push(data[el]);
                                  return rslt;
                              },
                              []
                          );
                      };
  
                      var dtf = $locale.DATETIME_FORMATS;
                      return {
                          monthNames : tValues(dtf.MONTH),
                          monthNamesShort : tValues(dtf.SHORTMONTH),
                          dayNames : tValues(dtf.DAY),
                          dayNamesShort : tValues(dtf.SHORTDAY)
                      };
                  }
  
                  return {};
              };
          }
      ])
      .directive('uiCalendar', ['uiCalendarConfig',
          function (uiCalendarConfig) {
  
              return {
                  restrict : 'A',
                  scope : {
                      eventSources : '=ngModel',
                      calendarWatchEvent : '&'
                  },
                  controller : 'uiCalendarCtrl',
  
  
                  link : function (scope, elm, attrs, controller) {
                      var sources = scope.eventSources;
                      var sourcesChanged = false;
                      var calendar;
                      var eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint);
                      var eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint);
                      var options = null;
  
                      function getOptions () {
                          var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {};
                          var fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);
                          var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
                          angular.extend(localeFullCalendarConfig, fullCalendarConfig);
                          options = {
                              eventSources : sources
                          };
                          angular.extend(options, localeFullCalendarConfig);
                          //remove calendars from options
                          options.calendars = null;
  
                          var options2 = {};
                          for (var o in options) {
                              if (o !== 'eventSources') {
                                  options2[o] = options[o];
                              }
                          }
                          return JSON.stringify(options2);
                      }
  
                      scope.destroyCalendar = function () {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar('destroy');
                          }
                          if (attrs.calendar) {
                              calendar = uiCalendarConfig.calendars[attrs.calendar] = angular.element(elm).html('');
                          } else {
                              calendar = angular.element(elm).html('');
                          }
                      };
  
                      scope.initCalendar = function () {
                          if (!calendar) {
                              calendar = $(elm).html('');
                          }
                          calendar.fullCalendar(options);
                          if (attrs.calendar) {
                              uiCalendarConfig.calendars[attrs.calendar] = calendar;
                          }
                      };
  
                      scope.$on('$destroy', function () {
                          scope.destroyCalendar();
                      });
  
                      eventSourcesWatcher.onAdded = function (source) {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar(options);
                              if (attrs.calendar) {
                                  uiCalendarConfig.calendars[attrs.calendar] = calendar;
                              }
                              calendar.fullCalendar('addEventSource', source);
                              sourcesChanged = true;
                          }
                      };
  
                      eventSourcesWatcher.onRemoved = function (source) {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar('removeEventSource', source);
                              sourcesChanged = true;
                          }
                      };
  
                      eventSourcesWatcher.onChanged = function () {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar('refetchEvents');
                              sourcesChanged = true;
                          }
                      };
  
                      eventsWatcher.onAdded = function (event) {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar('renderEvent', event, !!event.stick);
                          }
                      };
  
                      eventsWatcher.onRemoved = function (event) {
                          if (calendar && calendar.fullCalendar) {
                              calendar.fullCalendar('removeEvents', event._id);
                          }
                      };
  
                      eventsWatcher.onChanged = function (event) {
                          if (calendar && calendar.fullCalendar) {
                              var clientEvents = calendar.fullCalendar('clientEvents', event._id);
                              for (var i = 0; i < clientEvents.length; i++) {
                                  var clientEvent = clientEvents[i];
                                  clientEvent = angular.extend(clientEvent, event);
                                  calendar.fullCalendar('updateEvent', clientEvent);
                              }
                          }
                      };
  
                      eventSourcesWatcher.subscribe(scope);
                      eventsWatcher.subscribe(scope, function () {
                          if (sourcesChanged === true) {
                              sourcesChanged = false;
                              // return false to prevent onAdded/Removed/Changed handlers from firing in this case
                              return false;
                          }
                      });
  
                      scope.$watch(getOptions, function (newValue, oldValue) {
                          if (newValue !== oldValue) {
                              scope.destroyCalendar();
                              scope.initCalendar();
                          } else if ((newValue && angular.isUndefined(calendar))) {
                              scope.initCalendar();
                          }
                      });
                  }
              };
          }
      ]
  );
  