Dataset/JS/AngularCalendar/calendar.js (290 lines of code) (raw):
/*
* 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();
}
});
}
};
}
]
);