--- /dev/null
+/*
+* 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,
+ extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,
+
+ 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);
+ } else {
+ var args = arguments;
+ var self = this;
+ return $scope.$root.$apply(function(){
+ return functionToWrap.apply(self, 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;
+ };
+
+ var sourceSerialId = 1, sourceEventsSerialId = 1;
+ // @return {String} fingerprint of the source object and its events array
+ this.sourceFingerprint = function(source) {
+ var fp = '' + (source.__id || (source.__id = sourceSerialId++)),
+ 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() {
+ // do sources.map(&:events).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)) {
+ // event source as array
+ arraySources.push(source);
+ } else if(angular.isObject(source) && angular.isArray(source.events)){
+ // event source as object, ie extended form
+ var extEvent = {};
+ for(var key in source){
+ if(key !== '_id' && key !== 'events'){
+ extEvent[key] = source[key];
+ }
+ }
+ for(var eI = 0;eI < source.events.length;eI++){
+ angular.extend(source.events[eI],extEvent);
+ }
+ arraySources.push(source.events);
+ }
+ }
+ return Array.prototype.concat.apply([], arraySources);
+ };
+
+ // 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() {
+ var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
+ var result = [], token, el;
+ for (var i = 0, n = array.length; i < n; i++) {
+ el = array[i];
+ token = tokenFn(el);
+ map[token] = el;
+ result.push(token);
+ }
+ return result;
+ };
+
+ // @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 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 = {};
+
+ // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
+ 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, 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
+ };
+ };
+
+ 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.useNgLocale) {
+ // Configure to use locale names by default
+ var tValues = function(data) {
+ // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
+ var r, k;
+ r = [];
+ for (k in data) {
+ r[k] = data[k];
+ }
+ return r;
+ };
+ 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,
+ sourcesChanged = false,
+ calendar,
+ eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint),
+ eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint),
+ options = null;
+
+ function getOptions(){
+ var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {},
+ fullCalendarConfig;
+
+ 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] = $(elm).html('');
+ } else {
+ calendar = $(elm).html('');
+ }
+ };
+
+ scope.initCalendar = function(){
+ if (!calendar) {
+ calendar = angular.element(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 ? true : false));
+ }
+ };
+
+ 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();
+ }
+ });
+ }
+ };
+}]);