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