calendar.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. * AngularJs Fullcalendar Wrapper for the JQuery FullCalendar
  3. * API @ http://arshaw.com/fullcalendar/
  4. *
  5. * Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes.
  6. * Can also take in multiple event urls as a source object(s) and feed the events per view.
  7. * The calendar will watch any eventSource array and update itself when a change is made.
  8. *
  9. */
  10. angular.module('ui.calendar', [])
  11. .constant('uiCalendarConfig', {calendars: {}})
  12. .controller('uiCalendarCtrl', ['$scope',
  13. '$timeout',
  14. '$locale', function(
  15. $scope,
  16. $timeout,
  17. $locale){
  18. var sourceSerialId = 1,
  19. eventSerialId = 1,
  20. sources = $scope.eventSources,
  21. extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,
  22. wrapFunctionWithScopeApply = function(functionToWrap){
  23. var wrapper;
  24. if (functionToWrap){
  25. wrapper = function(){
  26. // This happens outside of angular context so we need to wrap it in a timeout which has an implied apply.
  27. // In this way the function will be safely executed on the next digest.
  28. var args = arguments;
  29. var _this = this;
  30. $timeout(function(){
  31. functionToWrap.apply(_this, args);
  32. });
  33. };
  34. }
  35. return wrapper;
  36. };
  37. this.eventsFingerprint = function(e) {
  38. if (!e._id) {
  39. e._id = eventSerialId++;
  40. }
  41. // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
  42. return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
  43. (e.allDay || '') + (e.className || '') + extraEventSignature(e) || '';
  44. };
  45. this.sourcesFingerprint = function(source) {
  46. return source.__id || (source.__id = sourceSerialId++);
  47. };
  48. this.allEvents = function() {
  49. // return sources.flatten(); but we don't have flatten
  50. var arraySources = [];
  51. for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
  52. var source = sources[i];
  53. if (angular.isArray(source)) {
  54. // event source as array
  55. arraySources.push(source);
  56. } else if(angular.isObject(source) && angular.isArray(source.events)){
  57. // event source as object, ie extended form
  58. var extEvent = {};
  59. for(var key in source){
  60. if(key !== '_uiCalId' && key !== 'events'){
  61. extEvent[key] = source[key];
  62. }
  63. }
  64. for(var eI = 0;eI < source.events.length;eI++){
  65. angular.extend(source.events[eI],extEvent);
  66. }
  67. arraySources.push(source.events);
  68. }
  69. }
  70. return Array.prototype.concat.apply([], arraySources);
  71. };
  72. // Track changes in array by assigning id tokens to each element and watching the scope for changes in those tokens
  73. // arguments:
  74. // arraySource array of function that returns array of objects to watch
  75. // tokenFn function(object) that returns the token for a given object
  76. this.changeWatcher = function(arraySource, tokenFn) {
  77. var self;
  78. var getTokens = function() {
  79. var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
  80. var result = [], token, el;
  81. for (var i = 0, n = array.length; i < n; i++) {
  82. el = array[i];
  83. token = tokenFn(el);
  84. map[token] = el;
  85. result.push(token);
  86. }
  87. return result;
  88. };
  89. // returns elements in that are in a but not in b
  90. // subtractAsSets([4, 5, 6], [4, 5, 7]) => [6]
  91. var subtractAsSets = function(a, b) {
  92. var result = [], inB = {}, i, n;
  93. for (i = 0, n = b.length; i < n; i++) {
  94. inB[b[i]] = true;
  95. }
  96. for (i = 0, n = a.length; i < n; i++) {
  97. if (!inB[a[i]]) {
  98. result.push(a[i]);
  99. }
  100. }
  101. return result;
  102. };
  103. // Map objects to tokens and vice-versa
  104. var map = {};
  105. var applyChanges = function(newTokens, oldTokens) {
  106. var i, n, el, token;
  107. var replacedTokens = {};
  108. var removedTokens = subtractAsSets(oldTokens, newTokens);
  109. for (i = 0, n = removedTokens.length; i < n; i++) {
  110. var removedToken = removedTokens[i];
  111. el = map[removedToken];
  112. delete map[removedToken];
  113. var newToken = tokenFn(el);
  114. // if the element wasn't removed but simply got a new token, its old token will be different from the current one
  115. if (newToken === removedToken) {
  116. self.onRemoved(el);
  117. } else {
  118. replacedTokens[newToken] = removedToken;
  119. self.onChanged(el);
  120. }
  121. }
  122. var addedTokens = subtractAsSets(newTokens, oldTokens);
  123. for (i = 0, n = addedTokens.length; i < n; i++) {
  124. token = addedTokens[i];
  125. el = map[token];
  126. if (!replacedTokens[token]) {
  127. self.onAdded(el);
  128. }
  129. }
  130. };
  131. return self = {
  132. subscribe: function(scope, onChanged) {
  133. scope.$watch(getTokens, function(newTokens, oldTokens) {
  134. if (!onChanged || onChanged(newTokens, oldTokens) !== false) {
  135. applyChanges(newTokens, oldTokens);
  136. }
  137. }, true);
  138. },
  139. onAdded: angular.noop,
  140. onChanged: angular.noop,
  141. onRemoved: angular.noop
  142. };
  143. };
  144. this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig){
  145. var config = {};
  146. angular.extend(config, uiCalendarConfig);
  147. angular.extend(config, calendarSettings);
  148. angular.forEach(config, function(value,key){
  149. if (typeof value === 'function'){
  150. config[key] = wrapFunctionWithScopeApply(config[key]);
  151. }
  152. });
  153. return config;
  154. };
  155. this.getLocaleConfig = function(fullCalendarConfig) {
  156. if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) {
  157. // Configure to use locale names by default
  158. var tValues = function(data) {
  159. // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
  160. var r, k;
  161. r = [];
  162. for (k in data) {
  163. r[k] = data[k];
  164. }
  165. return r;
  166. };
  167. var dtf = $locale.DATETIME_FORMATS;
  168. return {
  169. monthNames: tValues(dtf.MONTH),
  170. monthNamesShort: tValues(dtf.SHORTMONTH),
  171. dayNames: tValues(dtf.DAY),
  172. dayNamesShort: tValues(dtf.SHORTDAY)
  173. };
  174. }
  175. return {};
  176. };
  177. }])
  178. .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) {
  179. return {
  180. restrict: 'A',
  181. scope: {eventSources:'=ngModel',calendarWatchEvent: '&'},
  182. controller: 'uiCalendarCtrl',
  183. link: function(scope, elm, attrs, controller) {
  184. var sources = scope.eventSources,
  185. sourcesChanged = false,
  186. calendar,
  187. eventSourcesWatcher = controller.changeWatcher(sources, controller.sourcesFingerprint),
  188. eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventsFingerprint),
  189. options = null;
  190. function getOptions(){
  191. var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {},
  192. fullCalendarConfig;
  193. fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);
  194. var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
  195. angular.extend(localeFullCalendarConfig, fullCalendarConfig);
  196. options = { eventSources: sources };
  197. angular.extend(options, localeFullCalendarConfig);
  198. //remove calendars from options
  199. options.calendars = null;
  200. var options2 = {};
  201. for(var o in options){
  202. if(o !== 'eventSources'){
  203. options2[o] = options[o];
  204. }
  205. }
  206. return JSON.stringify(options2);
  207. }
  208. scope.destroy = function(){
  209. if(calendar && calendar.fullCalendar){
  210. calendar.fullCalendar('destroy');
  211. }
  212. if(attrs.calendar) {
  213. calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html('');
  214. } else {
  215. calendar = $(elm).html('');
  216. }
  217. };
  218. scope.init = function(){
  219. calendar.fullCalendar(options);
  220. };
  221. eventSourcesWatcher.onAdded = function(source) {
  222. calendar.fullCalendar('addEventSource', source);
  223. sourcesChanged = true;
  224. };
  225. eventSourcesWatcher.onRemoved = function(source) {
  226. calendar.fullCalendar('removeEventSource', source);
  227. sourcesChanged = true;
  228. };
  229. eventsWatcher.onAdded = function(event) {
  230. calendar.fullCalendar('renderEvent', event);
  231. };
  232. eventsWatcher.onRemoved = function(event) {
  233. calendar.fullCalendar('removeEvents', function(e) {
  234. return e._id === event._id;
  235. });
  236. };
  237. eventsWatcher.onChanged = function(event) {
  238. event._start = $.fullCalendar.moment(event.start);
  239. event._end = $.fullCalendar.moment(event.end);
  240. calendar.fullCalendar('updateEvent', event);
  241. };
  242. eventSourcesWatcher.subscribe(scope);
  243. eventsWatcher.subscribe(scope, function(newTokens, oldTokens) {
  244. if (sourcesChanged === true) {
  245. sourcesChanged = false;
  246. // prevent incremental updates in this case
  247. return false;
  248. }
  249. });
  250. scope.$watch(getOptions, function(newO,oldO){
  251. scope.destroy();
  252. scope.init();
  253. });
  254. }
  255. };
  256. }]);