From 5199d138d9136b3c8ad82a75ea8b43447ac154cb Mon Sep 17 00:00:00 2001 From: naveen Date: Fri, 24 Oct 2014 22:45:21 +0530 Subject: [PATCH 1/4] Fixed event ID assignment. Events with same IDs required for repeat funcionality were being assigned different '_id' 's earlier. --- src/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calendar.js b/src/calendar.js index 89abc17..a514c21 100644 --- a/src/calendar.js +++ b/src/calendar.js @@ -38,7 +38,7 @@ angular.module('ui.calendar', []) this.eventsFingerprint = function(e) { if (!e._id) { - e._id = eventSerialId++; + e._id = e.id ? e.id : ('ui-calendar' + ( eventSerialId++ ));//Search for event 'id' in the passed event object and assign if defined else assign generated id. } // 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 || '') + From df1e2b07b3e2a6001ef85172cce6019d51295a02 Mon Sep 17 00:00:00 2001 From: Naveen Adarsh Date: Sat, 25 Oct 2014 08:44:36 +0530 Subject: [PATCH 2/4] changed angular version to 1.3 in bower.json --- bower.json | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/bower.json b/bower.json index 21193ee..6c82ed7 100644 --- a/bower.json +++ b/bower.json @@ -1,27 +1,27 @@ { - "name": "angular-ui-calendar", - "version": "0.9.0-beta.1", - "description": "A complete AngularJS directive for the Arshaw FullCalendar.", - "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors", - "license": "MIT", - "homepage": "http://angular-ui.github.com", - "main": "./src/calendar.js", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test*", - "demo*", - "gruntFile.js", - "package.json" - ], - "dependencies": { - "angular": "~1.2.x", - "fullcalendar": "~2.x" - }, - "devDependencies": { - "angular-mocks": "~1.x", - "bootstrap-css": "2.3.1", - "jquery-ui": "~1.10.3" - } + "name" : "angular-ui-calendar", + "version" : "0.9.0-beta.1", + "description" : "A complete AngularJS directive for the Arshaw FullCalendar.", + "author" : "https://github.com/angular-ui/ui-calendar/graphs/contributors", + "license" : "MIT", + "homepage" : "http://angular-ui.github.com", + "main" : "./src/calendar.js", + "ignore" : [ + "**/.*", + "node_modules", + "bower_components", + "test*", + "demo*", + "gruntFile.js", + "package.json" + ], + "dependencies" : { + "angular" : "~1.3.x", + "fullcalendar" : "~2.x" + }, + "devDependencies" : { + "angular-mocks" : "~1.x", + "bootstrap-css" : "2.3.1", + "jquery-ui" : "~1.10.3" + } } From a17ea4d4b8c297d24ccd36c7b8259e112aed580e Mon Sep 17 00:00:00 2001 From: Naveen Adarsh Date: Sat, 25 Oct 2014 17:42:19 +0530 Subject: [PATCH 3/4] Fixed bug wherein event start and end dates were not being included in the token. Checked for presence of _i attribute in the event since updation from fullcalendar of the dates to moment dates caused triggering of onChanged event. --- src/calendar.js | 581 +++++++++++++++++++++++++----------------------- 1 file changed, 302 insertions(+), 279 deletions(-) diff --git a/src/calendar.js b/src/calendar.js index a514c21..d2dfc2a 100644 --- a/src/calendar.js +++ b/src/calendar.js @@ -1,282 +1,305 @@ /* -* 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. -* -*/ + * 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 = e.id ? e.id : ('ui-calendar' + ( eventSerialId++ ));//Search for event 'id' in the passed event object and assign if defined else assign generated id. - } - // 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) { - event._start = $.fullCalendar.moment(event.start); - event._end = $.fullCalendar.moment(event.end); - 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(); - }); - } - }; -}]); \ No newline at end of file +.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 = e.id ? e.id : ('ui-calendar' + (eventSerialId++)); //Search for event 'id' in the passed event object and assign if defined else assign generated id. + } + // 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.start._i || e.start ) : '') + + (e.end ? (e.end._i || 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) { + event._start = $.fullCalendar.moment(event.start); + event._end = $.fullCalendar.moment(event.end); + 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(); + }); + } + }; + } + ]); From 00e267cce4d7830227ca42d1f4a1321196c7c2bf Mon Sep 17 00:00:00 2001 From: Naveen Adarsh Date: Sat, 25 Oct 2014 17:50:51 +0530 Subject: [PATCH 4/4] Recommitting without changes to indentation and only the fix. --- src/calendar.js | 581 +++++++++++++++++++++++------------------------- 1 file changed, 279 insertions(+), 302 deletions(-) diff --git a/src/calendar.js b/src/calendar.js index d2dfc2a..6baf384 100644 --- a/src/calendar.js +++ b/src/calendar.js @@ -1,305 +1,282 @@ /* - * 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. - * - */ +* 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 = e.id ? e.id : ('ui-calendar' + (eventSerialId++)); //Search for event 'id' in the passed event object and assign if defined else assign generated id. - } - // 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.start._i || e.start ) : '') - + (e.end ? (e.end._i || 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) { - event._start = $.fullCalendar.moment(event.start); - event._end = $.fullCalendar.moment(event.end); - 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(); - }); - } - }; - } - ]); + .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 = e.id ? e.id : ('ui-calendar' + ( eventSerialId++ ));//Search for event 'id' in the passed event object and assign if defined else assign generated id. + } + // 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.start._i||e.start): '') + (e.end?(e.end._i||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) { + event._start = $.fullCalendar.moment(event.start); + event._end = $.fullCalendar.moment(event.end); + 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(); + }); + } + }; +}]); \ No newline at end of file