287 lines
12 KiB
Python
287 lines
12 KiB
Python
0/*
|
|
* 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', {})
|
|
.controller('uiCalendarCtrl', ['$scope', '$timeout', '$locale', function($scope, $timeout, $locale){
|
|
|
|
var sourceSerialId = 1,
|
|
eventSerialId = 1,
|
|
sources = $scope.eventSources,
|
|
extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,
|
|
|
|
wrapFunctionWithScopeApply = function(functionToWrap){
|
|
var wrapper;
|
|
|
|
if (functionToWrap){
|
|
wrapper = function(){
|
|
// This happens outside of angular context so we need to wrap it in a timeout which has an implied apply.
|
|
// In this way the function will be safely executed on the next digest.
|
|
|
|
var args = arguments;
|
|
var _this = this;
|
|
$timeout(function(){
|
|
functionToWrap.apply(_this, args);
|
|
});
|
|
};
|
|
}
|
|
|
|
return wrapper;
|
|
};
|
|
|
|
this.eventsFingerprint = function(e) {
|
|
if (!e._id) {
|
|
e._id = eventSerialId++;
|
|
}
|
|
// 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 || '') + (+e.start || '') + (+e.end || '') +
|
|
(e.allDay || '') + (e.className || '') + extraEventSignature(e) || '';
|
|
};
|
|
|
|
this.sourcesFingerprint = function(source) {
|
|
return source.__id || (source.__id = sourceSerialId++);
|
|
};
|
|
|
|
this.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)) {
|
|
// 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 !== '_uiCalId' && 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 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
|
|
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;
|
|
};
|
|
// 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, onChanged) {
|
|
scope.$watch(getTokens, function(newTokens, oldTokens) {
|
|
if (!onChanged || onChanged(newTokens, oldTokens) !== false) {
|
|
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,
|
|
eventSourcesWatcher = controller.changeWatcher(sources, controller.sourcesFingerprint),
|
|
eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventsFingerprint),
|
|
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);
|
|
|
|
var options2 = {};
|
|
for(var o in options){
|
|
if(o !== 'eventSources'){
|
|
options2[o] = options[o];
|
|
}
|
|
}
|
|
return JSON.stringify(options2);
|
|
}
|
|
|
|
scope.destroy = function(){
|
|
if(scope.calendar && scope.calendar.fullCalendar){
|
|
scope.calendar.fullCalendar('destroy');
|
|
}
|
|
if(attrs.calendar) {
|
|
scope.calendar = scope.$parent[attrs.calendar] = $(elm).html('');
|
|
} else {
|
|
scope.calendar = $(elm).html('');
|
|
}
|
|
};
|
|
|
|
scope.init = function(){
|
|
scope.calendar.fullCalendar(options);
|
|
};
|
|
|
|
eventSourcesWatcher.onAdded = function(source) {
|
|
scope.calendar.fullCalendar('addEventSource', source);
|
|
sourcesChanged = true;
|
|
};
|
|
|
|
eventSourcesWatcher.onRemoved = function(source) {
|
|
scope.calendar.fullCalendar('removeEventSource', source);
|
|
sourcesChanged = true;
|
|
};
|
|
|
|
eventsWatcher.onAdded = function(event) {
|
|
scope.calendar.fullCalendar('renderEvent', event);
|
|
};
|
|
|
|
eventsWatcher.onRemoved = function(event) {
|
|
scope.calendar.fullCalendar('removeEvents', function(e) {
|
|
return e._id === event._id;
|
|
});
|
|
};
|
|
|
|
eventsWatcher.onChanged = function(event) {
|
|
// Error - Object.eventsWatcher.onChanged
|
|
// event._start = $.fullCalendar.moment(event.start);
|
|
// event._end = $.fullCalendar.moment(event.end);
|
|
|
|
// Checking changes in the event
|
|
// console.log(event);
|
|
scope.calendar.fullCalendar('updateEvent', event);
|
|
|
|
};
|
|
|
|
eventSourcesWatcher.subscribe(scope);
|
|
eventsWatcher.subscribe(scope, function(newTokens, oldTokens) {
|
|
if (sourcesChanged === true) {
|
|
sourcesChanged = false;
|
|
// prevent incremental updates in this case
|
|
return false;
|
|
}
|
|
});
|
|
|
|
scope.$watch(getOptions, function(newO,oldO){
|
|
scope.destroy();
|
|
scope.init();
|
|
});
|
|
}
|
|
};
|
|
}]); |