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 11angular.module('ui.calendar', []) 12 13 .constant('uiCalendarConfig', { 14 calendars : {} 15 }) 16 .controller('uiCalendarCtrl', ['$scope', '$locale', 17 function ($scope, $locale) { 18 19 var sources = $scope.eventSources; 20 var extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop; 21 22 var wrapFunctionWithScopeApply = function (functionToWrap) { 23 return function () { 24 // This may happen outside of angular context, so create one if outside. 25 if ($scope.$root.$$phase) { 26 return functionToWrap.apply(this, arguments); 27 } 28 29 var args = arguments; 30 var that = this; 31 return $scope.$root.$apply( 32 function () { 33 return functionToWrap.apply(that, args); 34 } 35 ); 36 }; 37 }; 38 39 var eventSerialId = 1; 40 // @return {String} fingerprint of the event object and its properties 41 this.eventFingerprint = function (e) { 42 if (!e._id) { 43 e._id = eventSerialId++; 44 } 45 46 var extraSignature = extraEventSignature({ 47 event : e 48 }) || ''; 49 var start = moment.isMoment(e.start) ? e.start.unix() : (e.start ? moment(e.start).unix() : ''); 50 var end = moment.isMoment(e.end) ? e.end.unix() : (e.end ? moment(e.end).unix() : ''); 51 52 // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3 53 return [e._id, e.id || '', e.title || '', e.url || '', start, end, e.allDay || '', e.className || '', extraSignature].join(''); 54 }; 55 56 var sourceSerialId = 1; 57 var sourceEventsSerialId = 1; 58 // @return {String} fingerprint of the source object and its events array 59 this.sourceFingerprint = function (source) { 60 var fp = '' + (source.__id || (source.__id = sourceSerialId++)); 61 var events = angular.isObject(source) && source.events; 62 63 if (events) { 64 fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++)); 65 } 66 return fp; 67 }; 68 69 // @return {Array} all events from all sources 70 this.allEvents = function () { 71 return Array.prototype.concat.apply( 72 [], 73 (sources || []).reduce( 74 function (previous, source) { 75 if (angular.isArray(source)) { 76 previous.push(source); 77 } else if (angular.isObject(source) && angular.isArray(source.events)) { 78 var extEvent = Object.keys(source).filter( 79 function (key) { 80 return (key !== '_id' && key !== 'events'); 81 } 82 ); 83 84 source.events.forEach( 85 function (event) { 86 angular.extend(event, extEvent); 87 } 88 ); 89 90 previous.push(source.events); 91 } 92 return previous; 93 }, 94 [] 95 ) 96 ); 97 }; 98 99 // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens 100 // @param {Array|Function}
100 arraySource array of objects to watch 101 // @param tokenFn {Function} that returns the token for a given object 102 // @return {Object} 103 // subscribe: function(scope, function(newTokens, oldTokens)) 104 // called when source has changed. return false to prevent individual callbacks from firing 105 // onAdded/Removed/Changed: 106 // when set to a callback, called each item where a respective change is detected 107 this.changeWatcher = function (arraySource, tokenFn) { 108 var self; 109 110 var getTokens = function () { 111 return ((angular.isFunction(arraySource) ? arraySource() : arraySource) || []).reduce( 112 function (rslt, el) { 113 var token = tokenFn(el); 114 map[token] = el; 115 rslt.push(token); 116 return rslt; 117 }, 118 [] 119 ); 120 }; 121 122 // @param {Array} a 123 // @param {Array} b 124 // @return {Array} elements in that are in a but not in b 125 // @example 126 // subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100] 127 var subtractAsSets = function (a, b) { 128 var obj = (b || []).reduce( 129 function (rslt, val) { 130 rslt[val] = true; 131 return rslt; 132 }, 133 Object.create(null) 134 ); 135 return (a || []).filter( 136 function (val) { 137 return !obj[val]; 138 } 139 ); 140 }; 141 142 // Map objects to tokens and vice-versa 143 var map = {}; 144 145 // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively. 146 var applyChanges = function (newTokens, oldTokens) { 147 var i; 148 var token; 149 var replacedTokens = {}; 150 var removedTokens = subtractAsSets(oldTokens, newTokens); 151 for (i = 0; i < removedTokens.length; i++) { 152 var removedToken = removedTokens[i]; 153 var el = map[removedToken]; 154 delete map[removedToken]; 155 var newToken = tokenFn(el); 156 // if the element wasn't removed but simply got a new token, its old token will be different from the current one 157 if (newToken === removedToken) { 158 self.onRemoved(el); 159 } else { 160 replacedTokens[newToken] = removedToken; 161 self.onChanged(el); 162 } 163 } 164 165 var addedTokens = subtractAsSets(newTokens, oldTokens); 166 for (i = 0; i < addedTokens.length; i++) { 167 token = addedTokens[i]; 168 if (!replacedTokens[token]) { 169 self.onAdded(map[token]); 170 } 171 } 172 }; 173 174 self = { 175 subscribe : function (scope, onArrayChanged) { 176 scope.$watch(getTokens, function (newTokens, oldTokens) { 177 var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false); 178 if (notify) { 179 applyChanges(newTokens, oldTokens); 180 } 181 }, true); 182 }, 183 onAdded : angular.noop, 184 onChanged : angular.noop, 185 onRemoved : angular.noop 186 }; 187 return self; 188 }; 189 190 this.getFullCalendarConfig = function (calendarSettings, uiCalendarConfig) { 191 var config = {}; 192 193 angular.extend(config, uiCalendarConfig); 194 angular.extend(config, calendarSettings); 195 196 angular.forEach(config, function (value, key) { 197 if (typeof value === 'function') { 198 config[key] = wrapFunctionWithScopeApply(config[key]); 199 } 200 }); 201 202 return config; 203 }; 204 205 this.getLocaleConfig = function (fullCalendarConfig) { 206 if (!fullCalendarConfig.lang && !fullCalendarConfig.locale || fullCalendarConfig.useNgLocale) { 207 // Configure to use locale names by default 208 var tValues = function (data) { 209 // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...] 210 return (Object.keys(data) || []).reduce( 211 function (rslt, el) { 212 rslt.push(data[el]); 213 return rslt; 214 }, 215 [] 216 ); 217 }; 218 219 var dtf = $locale.DATETIME_FORMATS; 220 return { 221 monthNames : tValues(dtf.MONTH), 222 monthNamesShort : tValues(dtf.SHORTMONTH), 223 dayNames : tValues(dtf.DAY), 224 dayNamesShort : tValues(dtf.SHORTDAY) 225 }; 226 } 227 228 return {}; 229 }; 230 } 231 ]) 232 .directive('uiCalendar', ['uiCalendarConfig', 233 function (uiCalendarConfig) { 234 235 return { 236 restrict : 'A', 237 scope : { 238 eventSources : '=ngModel', 239 calendarWatchEvent : '&' 240 }, 241 controller : 'uiCalendarCtrl', 242 link : function (scope, elm, attrs, controller) { 243 var sources = scope.eventSources; 244 var sourcesChanged = false;
245 var calendar; 246 var eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint); 247 var eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint); 248 var options = null; 249 250 function getOptions () { 251 var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {}; 252 var fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig); 253 var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig); 254 angular.extend(localeFullCalendarConfig, fullCalendarConfig); 255 options = { 256 eventSources : sources 257 }; 258 angular.extend(options, localeFullCalendarConfig); 259 //remove calendars from options 260 options.calendars = null; 261 262 var options2 = {}; 263 for (var o in options) { 264 if (o !== 'eventSources') { 265 options2[o] = options[o]; 266 } 267 } 268 return JSON.stringify(options2); 269 } 270 271 scope.destroyCalendar = function () { 272 if (calendar && calendar.fullCalendar) { 273 calendar.fullCalendar('destroy'); 274 } 275 if (attrs.calendar) { 276 calendar = uiCalendarConfig.calendars[attrs.calendar] = angular.element(elm).html(''); 277 } else { 278 calendar = angular.element(elm).html(''); 279 } 280 }; 281 282 scope.initCalendar = function () { 283 if (!calendar) { 284 calendar = $(elm).html(''); 285 } 286 calendar.fullCalendar(options); 287 if (attrs.calendar) { 288 uiCalendarConfig.calendars[attrs.calendar] = calendar; 289 } 290 }; 291 292 scope.$on('$destroy', function () { 293 scope.destroyCalendar(); 294 }); 295 296 eventSourcesWatcher.onAdded = function (source) { 297 if (calendar && calendar.fullCalendar) { 298 calendar.fullCalendar(options); 299 if (attrs.calendar) { 300 uiCalendarConfig.calendars[attrs.calendar] = calendar; 301 } 302 calendar.fullCalendar('addEventSource', source); 303 sourcesChanged = true; 304 } 305 }; 306 307 eventSourcesWatcher.onRemoved = function (source) { 308 if (calendar && calendar.fullCalendar) { 309 calendar.fullCalendar('removeEventSource', source); 310 sourcesChanged = true; 311 } 312 }; 313 314 eventSourcesWatcher.onChanged = function () { 315 if (calendar && calendar.fullCalendar) { 316 calendar.fullCalendar('refetchEvents'); 317 sourcesChanged = true; 318 } 319 }; 320 321 eventsWatcher.onAdded = function (event) { 322 if (calendar && calendar.fullCalendar) { 323 calendar.fullCalendar('renderEvent', event, !!event.stick); 324 } 325 }; 326 327 eventsWatcher.onRemoved = function (event) { 328 if (calendar && calendar.fullCalendar) { 329 calendar.fullCalendar('removeEvents', event._id); 330 } 331 }; 332 333 eventsWatcher.onChanged = function (event) { 334 if (calendar && calendar.fullCalendar) { 335 var clientEvents = calendar.fullCalendar('clientEvents', event._id); 336 for (var i = 0; i < clientEvents.length; i++) { 337 var clientEvent = clientEvents[i]; 338 clientEvent = angular.extend(clientEvent, event); 339 calendar.fullCalendar('updateEvent', clientEvent); 340 } 341 } 342 }; 343 344 eventSourcesWatcher.subscribe(scope); 345 eventsWatcher.subscribe(scope, function () { 346 if (sourcesChanged === true) { 347 sourcesChanged = false;
348 // return false to prevent onAdded/Removed/Changed handlers from firing in this case 349 return false; 350 } 351 }); 352 353 scope.$watch(getOptions, function (newValue, oldValue) { 354 if (newValue !== oldValue) { 355 scope.destroyCalendar(); 356 scope.initCalendar(); 357 } else if ((newValue && angular.isUndefined(calendar))) { 358 scope.initCalendar(); 359 } 360 }); 361 } 362 }; 363 } 364 ] 365);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.