· 7 years ago · Oct 25, 2018, 12:26 AM
1/*
2 * Copyright (c) 2009 Rob Monie
3 * Copyright (c) 2010 Julien MUETTON
4 * Dual licensed under the MIT and GPL licenses:
5 * http://www.opensource.org/licenses/mit-license.php
6 * http://www.gnu.org/licenses/gpl.html
7
8 */
9
10 /*variable to store searched student */
11 var studentID = $('#searchQuery').val();
12
13 var year = new Date().getFullYear();
14 var month = new Date().getMonth();
15 var day = new Date().getDate();
16
17
18 var eventData = {
19 options: {
20 timeslotHeight: 30
21 },
22 events : [
23 {'id':1, 'start': new Date(year, month, day, 9), 'end': new Date(year, month, day, 11),'title':'Music Technology Foundations (30) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406'},
24 {'id':2, 'start': new Date(year, month, day - 2, 10), 'end': new Date(year, month, day -2, 12),'title':'Circuit Bending and Hardware Hacking (42) 8, 9, 10, 11, 12, 13, 17, 18, 20, 21, 22 Schulz 506'},
25 {'id':3, 'start': new Date(year, month, day + 3, 11), 'end': new Date(year, month, day + 3, 12),'title':'Sound Design for Games III (20) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406'},
26 {'id':4, 'start': new Date(year, month, day + 2, 10), 'end': new Date(year, month, day + 2, 12),'title':'Music 2.0(25) 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 Schulz 406'},
27 {'id':5, 'start': new Date(year, month, day + 3, 14), 'end': new Date(year, month, day + 3, 15),'title':'Music 2.0(25) 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 Schulz 406'},
28 {'id':6, 'start': new Date(year, month, day + 1, 14), 'end': new Date(year, month, day + 1, 18),'title':'Sound Design for Games III (20) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406'},
29 {'id':7, 'start': new Date(year, month, day + 1, 14), 'end': new Date(year, month, day + 1, 16),'title':'Music Technology Foundations (30) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406'},
30 {'id':8, 'start': new Date(year, month, day - 3, 12), 'end': new Date(year, month, day - 3, 15),'title':'Circuit Bending and Hardware Hacking (42) 8, 9, 10, 11, 12, 13, 17, 18, 20, 21, 22 Schulz 506'},
31 {'id':9, 'start': new Date(year, month, day - 2, 15), 'end': new Date(year, month, day - 2, 18),'title':'Sound Design for Games III (20) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406' },
32 {'id':10, 'start': new Date(year, month, day + 1, 11), 'end': new Date(year, month, day + 1, 12),'title':'Music 2.0(25) 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 Schulz 406'},
33 {'id':11, 'start': new Date(year, month, day + 4, 15), 'end': new Date(year, month, day + 4, 18),'title':'Sound Design for Games III (20) 8, 9, 10, 11, 12, 16, 17, 19, 20, 21 Schulz 406'}
34 ]
35 };
36
37 $(document).ready(function() {
38 $('#calendar').weekCalendar({
39 timeslotsPerHour: 4,
40 timeslotHeigh: 30,
41 hourLine: true,
42 data: eventData,
43 /*custom settings >>>*/
44 readonly: true,
45 switchDisplay: {'1 day': 1,'3 days': 3,'Work week': 5, 'Full week': 7},
46 businessHours: {start: 8, end: 20, limitDisplay: true},
47 allowCalEventOverlap: true,
48 overlapEventsSeparate: true,
49
50 /*<<<<<*/
51 height: function($calendar) {
52 return $(window).height() - $('h1').outerHeight(true);
53 },
54 eventRender : function(calEvent, $event) {
55 if (calEvent.end.getTime() < new Date().getTime()) {
56 $event.css('backgroundColor', '#aaa');
57 $event.find('.time').css({'backgroundColor': '#999', 'border':'1px solid #888'});
58 }
59 },
60 eventNew: function(calEvent, $event) {
61 displayMessage('<strong>Added event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
62 alert('You\'ve added a new event. You would capture this event, add the logic for creating a new event with your own fields, data and whatever backend persistence you require.');
63 },
64 eventDrop: function(calEvent, $event) {
65 displayMessage('<strong>Moved Event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
66 },
67 eventResize: function(calEvent, $event) {
68 displayMessage('<strong>Resized Event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
69 },
70 eventClick: function(calEvent, $event) {
71 displayMessage('<strong>Clicked Event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
72 },
73 eventMouseover: function(calEvent, $event) {
74 displayMessage('<strong>Mouseover Event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
75 },
76 eventMouseout: function(calEvent, $event) {
77 displayMessage('<strong>Mouseout Event</strong><br/>Start: ' + calEvent.start + '<br/>End: ' + calEvent.end);
78 },
79 noEvents: function() {
80 displayMessage('There are no events for this week');
81 }
82 });
83
84 /*function displayMessage(message) {
85 $('#message').html(message).fadeIn();
86 }*/
87
88 //$('<div id="message" class="ui-corner-all"></div>').prependTo($('body'));
89 });
90
91
92
93
94
95(function($) {
96 // check the jquery version
97 var _v = $.fn.jquery.split('.'),
98 _jQuery14OrLower = (10 * _v[0] + _v[1]) < 15;
99
100 $.widget('ui.weekCalendar', (function() {
101 var _currentAjaxCall, _hourLineTimeout;
102
103 return {
104 options: {
105 date: new Date(),
106 timeFormat: null,
107 dateFormat: 'M d, Y',
108 alwaysDisplayTimeMinutes: true,
109 use24Hour: false,
110 daysToShow: 7,
111 minBodyHeight: 100,
112 firstDayOfWeek: function(calendar) {
113 if ($(calendar).weekCalendar('option', 'daysToShow') != 5) {
114 return 0;
115 } else {
116 //workweek
117 return 1;
118 }
119 }, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday
120 useShortDayNames: false,
121 timeSeparator: ' to ',
122 startParam: 'start',
123 endParam: 'end',
124 businessHours: {start: 8, end: 18, limitDisplay: false},
125 newEventText: 'New Event',
126 timeslotHeight: 20,
127 defaultEventLength: 2,
128 timeslotsPerHour: 4,
129 minDate: null,
130 maxDate: null,
131 showHeader: true,
132 buttons: true,
133 buttonText: {
134 today: 'today',
135 lastWeek: 'previous',
136 nextWeek: 'next'
137 },
138 switchDisplay: {},
139 scrollToHourMillis: 500,
140 allowEventDelete: false,
141 allowCalEventOverlap: false,
142 overlapEventsSeparate: false,
143 totalEventsWidthPercentInOneColumn: 100,
144 readonly: false,
145 allowEventCreation: true,
146 hourLine: false,
147 deletable: function(calEvent, element) {
148 return true;
149 },
150 draggable: function(calEvent, element) {
151 return true;
152 },
153 resizable: function(calEvent, element) {
154 return true;
155 },
156 eventClick: function(calEvent, element, dayFreeBusyManager,
157 calendar, clickEvent) {
158 },
159 eventRender: function(calEvent, element) {
160 return element;
161 },
162 eventAfterRender: function(calEvent, element) {
163 return element;
164 },
165 eventRefresh: function(calEvent, element) {
166 return element;
167 },
168 eventDrag: function(calEvent, element) {
169 },
170 eventDrop: function(calEvent, element) {
171 },
172 eventResize: function(calEvent, element) {
173 },
174 eventNew: function(calEvent, element, dayFreeBusyManager,
175 calendar, mouseupEvent) {
176 },
177 eventMouseover: function(calEvent, $event) {
178 },
179 eventMouseout: function(calEvent, $event) {
180 },
181 eventDelete: function(calEvent, element, dayFreeBusyManager,
182 calendar, clickEvent) {
183 calendar.weekCalendar('removeEvent',calEvent.id);
184 },
185 calendarBeforeLoad: function(calendar) {
186 },
187 calendarAfterLoad: function(calendar) {
188 },
189 noEvents: function() {
190 },
191 eventHeader: function(calEvent, calendar) {
192 var options = calendar.weekCalendar('option');
193 var one_hour = 3600000;
194 var displayTitleWithTime = calEvent.end.getTime() - calEvent.start.getTime() <= (one_hour / options.timeslotsPerHour);
195 if (displayTitleWithTime) {
196 return calendar.weekCalendar(
197 'formatTime', calEvent.start) +
198 ': ' + calEvent.title;
199 } else {
200 return calendar.weekCalendar(
201 'formatTime', calEvent.start) +
202 options.timeSeparator +
203 calendar.weekCalendar(
204 'formatTime', calEvent.end);
205 }
206 },
207 eventBody: function(calEvent, calendar) {
208 return calEvent.title;
209 },
210 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
211 longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
212 shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
213 longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
214 /* multi-users options */
215 /**
216 * the available users for calendar.
217 * if you want to display users separately, enable the
218 * showAsSeparateUsers option.
219 * if you provide a list of user and do not enable showAsSeparateUsers
220 * option, then only the events that belongs to one or several of
221 * given users will be displayed
222 * @type {array}
223 */
224 users: [],
225 /**
226 * should the calendar be displayed with separate column for each
227 * users.
228 * note that this option does nothing if you do not provide at least
229 * one user.
230 * @type {boolean}
231 */
232 showAsSeparateUsers: true,
233 /**
234 * callback used to read user id from a user object.
235 * @param {Object} user the user to retrieve the id from.
236 * @param {number} index the user index from user list.
237 * @param {jQuery} calendar the calendar object.
238 * @return {int|String} the user id.
239 */
240 getUserId: function(user, index, calendar) {
241 return index;
242 },
243 /**
244 * callback used to read user name from a user object.
245 * @param {Object} user the user to retrieve the name from.
246 * @param {number} index the user index from user list.
247 * @param {jQuery} calendar the calendar object.
248 * @return {String} the user name.
249 */
250 getUserName: function(user, index, calendar) {
251 return user;
252 },
253 /**
254 * reads the id(s) of user(s) for who the event should be displayed.
255 * @param {Object} calEvent the calEvent to read informations from.
256 * @param {jQuery} calendar the calendar object.
257 * @return {number|String|Array} the user id(s) to appened events for.
258 */
259 getEventUserId: function(calEvent, calendar) {
260 return calEvent.userId;
261 },
262 /**
263 * sets user id(s) to the calEvent
264 * @param {Object} calEvent the calEvent to set informations to.
265 * @param {jQuery} calendar the calendar object.
266 * @return {Object} the calEvent with modified user id.
267 */
268 setEventUserId: function(userId, calEvent, calendar) {
269 calEvent.userId = userId;
270 return calEvent;
271 },
272 /* freeBusy options */
273 /**
274 * should the calendar display freebusys ?
275 * @type {boolean}
276 */
277 displayFreeBusys: false,
278 /**
279 * read the id(s) for who the freebusy is available
280 * @param {Object} calEvent the calEvent to read informations from.
281 * @param {jQuery} calendar the calendar object.
282 * @return {number|String|Array} the user id(s) to appened events for.
283 */
284 getFreeBusyUserId: function(calFreeBusy, calendar) {
285 return calFreeBusy.userId;
286 },
287 /**
288 * the default freeBusy object, used to manage default state
289 * @type {Object}
290 */
291 defaultFreeBusy: {free: false},
292 /**
293 * function used to display the freeBusy element
294 * @type {Function}
295 * @param {Object} freeBusy the freeBusy timeslot to render.
296 * @param {jQuery} $freeBusy the freeBusy HTML element.
297 * @param {jQuery} calendar the calendar element.
298 */
299 freeBusyRender: function(freeBusy, $freeBusy, calendar) {
300 if (!freeBusy.free) {
301 $freeBusy.addClass('free-busy-busy');
302 }
303 else {
304 $freeBusy.addClass('free-busy-free');
305 }
306 return $freeBusy;
307 },
308 /* other options */
309 /**
310 * true means start on first day of week, false means starts on
311 * startDate.
312 * @param {jQuery} calendar the calendar object.
313 * @type {Function|bool}
314 */
315 startOnFirstDayOfWeek: function(calendar) {
316 return $(calendar).weekCalendar('option', 'daysToShow') >= 5;
317 },
318 /**
319 * should the columns be rendered alternatively using odd/even
320 * class
321 * @type {boolean}
322 */
323 displayOddEven: false,
324 textSize: 13,
325 /**
326 * the title attribute for the calendar. possible placeholders are:
327 * <ul>
328 * <li>%start%</li>
329 * <li>%end%</li>
330 * <li>%date%</li>
331 * </ul>
332 * @type {Function|string}
333 * @param {number} option daysToShow.
334 * @return {String} the title attribute for the calendar.
335 */
336 title: '%start% - %end%',
337 /**
338 * default options to pass to callback
339 * you can pass a function returning an object or a litteral object
340 * @type {object|function(#calendar)}
341 */
342 jsonOptions: {},
343 headerSeparator: '<br />',
344 /**
345 * returns formatted header for day display
346 * @type {function(date,calendar)}
347 */
348 getHeaderDate: null,
349 preventDragOnEventCreation: false,
350 /**
351 * the event on which to bind calendar resize
352 * @type {string}
353 */
354 resizeEvent: 'resize.weekcalendar'
355 },
356
357 /***********************
358 * Initialise calendar *
359 ***********************/
360 _create: function() {
361 var self = this;
362 self._computeOptions();
363 self._setupEventDelegation();
364 self._renderCalendar();
365 self._loadCalEvents();
366 self._resizeCalendar();
367 self._scrollToHour(self.options.date.getHours(), true);
368
369 if (this.options.resizeEvent) {
370 $(window).unbind(this.options.resizeEvent);
371 $(window).bind(this.options.resizeEvent, function() {
372 self._resizeCalendar();
373 });
374 }
375
376 },
377
378 /********************
379 * public functions *
380 ********************/
381 /*
382 * Refresh the events for the currently displayed week.
383 */
384 refresh: function() {
385 //reload with existing week
386 this._loadCalEvents(this.element.data('startDate'));
387 },
388
389 /*
390 * Clear all events currently loaded into the calendar
391 */
392 clear: function() {
393 this._clearCalendar();
394 },
395
396 /*
397 * Go to this week
398 */
399 today: function() {
400 this._clearCalendar();
401 this._loadCalEvents(new Date());
402 },
403
404 /*
405 * Go to the previous week relative to the currently displayed week
406 */
407 prevWeek: function() {
408 //minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies
409 var newDate = new Date(this.element.data('startDate').getTime() - (MILLIS_IN_WEEK / 6));
410 this._clearCalendar();
411 this._loadCalEvents(newDate);
412 },
413
414 /*
415 * Go to the next week relative to the currently displayed week
416 */
417 nextWeek: function() {
418 //add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies
419 var newDate = new Date(this.element.data('startDate').getTime() + MILLIS_IN_WEEK + MILLIS_IN_DAY);
420 this._clearCalendar();
421 this._loadCalEvents(newDate);
422 },
423
424 /*
425 * Reload the calendar to whatever week the date passed in falls on.
426 */
427 gotoWeek: function(date) {
428 this._clearCalendar();
429 this._loadCalEvents(date);
430 },
431
432 /*
433 * Reload the calendar to whatever week the date passed in falls on.
434 */
435 gotoDate: function(date) {
436 this._clearCalendar();
437 this._loadCalEvents(date);
438 },
439
440 /**
441 * change the number of days to show
442 */
443 setDaysToShow: function(daysToShow) {
444 var self = this;
445 var hour = self._getCurrentScrollHour();
446 self.options.daysToShow = daysToShow;
447 $(self.element).html('');
448 self._renderCalendar();
449 self._loadCalEvents();
450 self._resizeCalendar();
451 self._scrollToHour(hour, false);
452
453 if (this.options.resizeEvent) {
454 $(window).unbind(this.options.resizeEvent);
455 $(window).bind(this.options.resizeEvent, function() {
456 self._resizeCalendar();
457 });
458 }
459 },
460
461 /*
462 * Remove an event based on it's id
463 */
464 removeEvent: function(eventId) {
465
466 var self = this;
467
468 self.element.find('.wc-cal-event').each(function() {
469 if ($(this).data('calEvent').id === eventId) {
470 $(this).remove();
471 return false;
472 }
473 });
474
475 //this could be more efficient rather than running on all days regardless...
476 self.element.find('.wc-day-column-inner').each(function() {
477 self._adjustOverlappingEvents($(this));
478 });
479 },
480
481 /*
482 * Removes any events that have been added but not yet saved (have no id).
483 * This is useful to call after adding a freshly saved new event.
484 */
485 removeUnsavedEvents: function() {
486
487 var self = this;
488
489 self.element.find('.wc-new-cal-event').each(function() {
490 $(this).remove();
491 });
492
493 //this could be more efficient rather than running on all days regardless...
494 self.element.find('.wc-day-column-inner').each(function() {
495 self._adjustOverlappingEvents($(this));
496 });
497 },
498
499 /*
500 * update an event in the calendar. If the event exists it refreshes
501 * it's rendering. If it's a new event that does not exist in the calendar
502 * it will be added.
503 */
504 updateEvent: function(calEvent) {
505 this._updateEventInCalendar(calEvent);
506 },
507
508 /*
509 * Returns an array of timeslot start and end times based on
510 * the configured grid of the calendar. Returns in both date and
511 * formatted time based on the 'timeFormat' config option.
512 */
513 getTimeslotTimes: function(date) {
514 var options = this.options;
515 var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
516 var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed);
517
518 var times = [],
519 startMillis = startDate.getTime();
520 for (var i = 0; i < options.timeslotsPerDay; i++) {
521 var endMillis = startMillis + options.millisPerTimeslot;
522 times[i] = {
523 start: new Date(startMillis),
524 startFormatted: this.formatTime(new Date(startMillis), options.timeFormat),
525 end: new Date(endMillis),
526 endFormatted: this.formatTime(new Date(endMillis), options.timeFormat)
527 };
528 startMillis = endMillis;
529 }
530 return times;
531 },
532
533 formatDate: function(date, format) {
534 if (format) {
535 return this._formatDate(date, format);
536 } else {
537 return this._formatDate(date, this.options.dateFormat);
538 }
539 },
540
541 formatTime: function(date, format) {
542 if (format) {
543 return this._formatDate(date, format);
544 } else if (this.options.timeFormat) {
545 return this._formatDate(date, this.options.timeFormat);
546 } else if (this.options.use24Hour) {
547 return this._formatDate(date, 'H:i');
548 } else {
549 return this._formatDate(date, 'h:i a');
550 }
551 },
552
553 serializeEvents: function() {
554 var self = this;
555 var calEvents = [];
556
557 self.element.find('.wc-cal-event').each(function() {
558 calEvents.push($(this).data('calEvent'));
559 });
560 return calEvents;
561 },
562
563 next: function() {
564 if (this._startOnFirstDayOfWeek()) {
565 return this.nextWeek();
566 }
567 var newDate = new Date(this.element.data('startDate').getTime());
568 newDate.setDate(newDate.getDate() + this.options.daysToShow);
569
570 this._clearCalendar();
571 this._loadCalEvents(newDate);
572 },
573
574 prev: function() {
575 if (this._startOnFirstDayOfWeek()) {
576 return this.prevWeek();
577 }
578 var newDate = new Date(this.element.data('startDate').getTime());
579 newDate.setDate(newDate.getDate() - this.options.daysToShow);
580
581 this._clearCalendar();
582 this._loadCalEvents(newDate);
583 },
584 getCurrentFirstDay: function() {
585 return this._dateFirstDayOfWeek(this.options.date || new Date());
586 },
587 getCurrentLastDay: function() {
588 return this._addDays(this.getCurrentFirstDay(), this.options.daysToShow - 1);
589 },
590
591 /*********************
592 * private functions *
593 *********************/
594 _setOption: function(key, value) {
595 var self = this;
596 if (self.options[key] != value) {
597 // event callback change, no need to re-render the events
598 if (key == 'beforeEventNew') {
599 self.options[key] = value;
600 return;
601 }
602
603 // this could be made more efficient at some stage by caching the
604 // events array locally in a store but this should be done in conjunction
605 // with a proper binding model.
606
607 var currentEvents = self.element.find('.wc-cal-event').map(function() {
608 return $(this).data('calEvent');
609 });
610
611 var newOptions = {};
612 newOptions[key] = value;
613 self._renderEvents({events: currentEvents, options: newOptions}, self.element.find('.wc-day-column-inner'));
614 }
615 },
616
617 // compute dynamic options based on other config values
618 _computeOptions: function() {
619 var options = this.options;
620 if (options.businessHours.limitDisplay) {
621 options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start);
622 options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 3600000; // 60 * 60 * 1000
623 options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay;
624 } else {
625 options.timeslotsPerDay = options.timeslotsPerHour * 24;
626 options.millisToDisplay = MILLIS_IN_DAY;
627 options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay;
628 }
629 },
630
631 /*
632 * Resize the calendar scrollable height based on the provided function in options.
633 */
634 _resizeCalendar: function() {
635 var options = this.options;
636 if (options && $.isFunction(options.height)) {
637 var calendarHeight = options.height(this.element);
638 var headerHeight = this.element.find('.wc-header').outerHeight();
639 var navHeight = this.element.find('.wc-toolbar').outerHeight();
640 var scrollContainerHeight = Math.max(calendarHeight - navHeight - headerHeight, options.minBodyHeight);
641 var timeslotHeight = this.element.find('.wc-time-slots').outerHeight();
642 this.element.find('.wc-scrollable-grid').height(scrollContainerHeight);
643 if (timeslotHeight <= scrollContainerHeight) {
644 this.element.find('.wc-scrollbar-shim').width(0);
645 }
646 else {
647 this.element.find('.wc-scrollbar-shim').width(this._findScrollBarWidth());
648 }
649 this._trigger('resize', this.element);
650 }
651 },
652
653 _findScrollBarWidth: function() {
654 var parent = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').appendTo('body');
655 var child = parent.children();
656 var width = child.innerWidth() - child.height(99).innerWidth();
657 parent.remove();
658 return width || /* default to 16 that is the average */ 16;
659 },
660
661 /*
662 * configure calendar interaction events that are able to use event
663 * delegation for greater efficiency
664 */
665 _setupEventDelegation: function() {
666 var self = this;
667 var options = this.options;
668
669 this.element.click(function(event) {
670 var $target = $(event.target),
671 freeBusyManager;
672
673 // click is disabled
674 if ($target.data('preventClick')) {
675 return;
676 }
677
678 var $calEvent = $target.hasClass('wc-cal-event') ?
679 $target :
680 $target.parents('.wc-cal-event');
681 if (!$calEvent.length || !$calEvent.data('calEvent')) {
682 return;
683 }
684
685 freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent'));
686
687 if (options.allowEventDelete && $target.hasClass('wc-cal-event-delete')) {
688 options.eventDelete($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
689 } else {
690 options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
691 }
692 }).mouseover(function(event) {
693 var $target = $(event.target);
694 var $calEvent = $target.hasClass('wc-cal-event') ?
695 $target :
696 $target.parents('.wc-cal-event');
697
698 if (!$calEvent.length || !$calEvent.data('calEvent')) {
699 return;
700 }
701
702 if (self._isDraggingOrResizing($calEvent)) {
703 return;
704 }
705
706 options.eventMouseover($calEvent.data('calEvent'), $calEvent, event);
707 }).mouseout(function(event) {
708 var $target = $(event.target);
709 var $calEvent = $target.hasClass('wc-cal-event') ?
710 $target :
711 $target.parents('.wc-cal-event');
712
713 if (!$calEvent.length || !$calEvent.data('calEvent')) {
714 return;
715 }
716
717 if (self._isDraggingOrResizing($calEvent)) {
718 return;
719 }
720
721 options.eventMouseout($calEvent.data('calEvent'), $calEvent, event);
722 });
723 },
724
725 /**
726 * check if a ui draggable or resizable is currently being dragged or
727 * resized.
728 */
729 _isDraggingOrResizing: function($target) {
730 return $target.hasClass('ui-draggable-dragging') ||
731 $target.hasClass('ui-resizable-resizing');
732 },
733
734 /*
735 * Render the main calendar layout
736 */
737 _renderCalendar: function() {
738 var $calendarContainer, $weekDayColumns;
739 var self = this;
740 var options = this.options;
741
742 $calendarContainer = $('<div class=\"ui-widget wc-container\">').appendTo(self.element);
743
744 //render the different parts
745 // nav links
746 self._renderCalendarButtons($calendarContainer);
747 // header
748 self._renderCalendarHeader($calendarContainer);
749 // body
750 self._renderCalendarBody($calendarContainer);
751
752 $weekDayColumns = $calendarContainer.find('.wc-day-column-inner');
753 $weekDayColumns.each(function(i, val) {
754 if (!options.readonly) {
755 self._addDroppableToWeekDay($(this));
756 if (options.allowEventCreation) {
757 self._setupEventCreationForWeekDay($(this));
758 }
759 }
760 });
761 },
762
763 /**
764 * render the nav buttons on top of the calendar
765 */
766 _renderCalendarButtons: function($calendarContainer) {
767 var self = this, options = this.options;
768 if ( !options.showHeader ) return;
769 if (options.buttons) {
770 var calendarNavHtml = '';
771
772 calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
773 calendarNavHtml += '<div class=\"wc-display\"></div>';
774 calendarNavHtml += '<div class=\"wc-nav\">';
775 calendarNavHtml += '<button class=\"wc-prev\">' + options.buttonText.lastWeek + '</button>';
776 calendarNavHtml += '<button class=\"wc-today\">' + options.buttonText.today + '</button>';
777 calendarNavHtml += '<button class=\"wc-next\">' + options.buttonText.nextWeek + '</button>';
778 calendarNavHtml += '</div>';
779 calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
780 calendarNavHtml += '</div>';
781
782 $(calendarNavHtml).appendTo($calendarContainer);
783
784 $calendarContainer.find('.wc-nav .wc-today')
785 .button({
786 icons: {primary: 'ui-icon-home'}})
787 .click(function() {
788 self.today();
789 return false;
790 });
791
792 $calendarContainer.find('.wc-nav .wc-prev')
793 .button({
794 text: false,
795 icons: {primary: 'ui-icon-seek-prev'}})
796 .click(function() {
797 self.element.weekCalendar('prev');
798 return false;
799 });
800
801 $calendarContainer.find('.wc-nav .wc-next')
802 .button({
803 text: false,
804 icons: {primary: 'ui-icon-seek-next'}})
805 .click(function() {
806 self.element.weekCalendar('next');
807 return false;
808 });
809
810 // now add buttons to switch display
811 if (this.options.switchDisplay && $.isPlainObject(this.options.switchDisplay)) {
812 var $container = $calendarContainer.find('.wc-display');
813 $.each(this.options.switchDisplay, function(label, option) {
814 var _id = 'wc-switch-display-' + option;
815 var _input = $('<input type="radio" id="' + _id + '" name="wc-switch-display" class="wc-switch-display"/>');
816 var _label = $('<label for="' + _id + '"></label>');
817 _label.html(label);
818 _input.val(option);
819 if (parseInt(self.options.daysToShow, 10) === parseInt(option, 10)) {
820 _input.attr('checked', 'checked');
821 }
822 $container
823 .append(_input)
824 .append(_label);
825 });
826 $container.find('input').change(function() {
827 self.setDaysToShow(parseInt($(this).val(), 10));
828 });
829 }
830 $calendarContainer.find('.wc-nav, .wc-display').buttonset();
831 var _height = $calendarContainer.find('.wc-nav').outerHeight();
832 $calendarContainer.find('.wc-title')
833 .height(_height)
834 .css('line-height', _height + 'px');
835 }else{
836 var calendarNavHtml = '';
837 calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
838 calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
839 calendarNavHtml += '</div>';
840 $(calendarNavHtml).appendTo($calendarContainer);
841
842 }
843 },
844
845 /**
846 * render the calendar header, including date and user header
847 */
848 _renderCalendarHeader: function($calendarContainer) {
849 var self = this, options = this.options,
850 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
851 rowspan = '', colspan = '', calendarHeaderHtml;
852
853 if (showAsSeparatedUser) {
854 rowspan = ' rowspan=\"2\"';
855 colspan = ' colspan=\"' + options.users.length + '\" ';
856 }
857
858 //first row
859 calendarHeaderHtml = '<div class=\"ui-widget-content wc-header\">';
860 calendarHeaderHtml += '<table><tbody><tr><td class=\"wc-time-column-header\"></td>';
861 for (var i = 1; i <= options.daysToShow; i++) {
862 calendarHeaderHtml += '<td class=\"wc-day-column-header wc-day-' + i + '\"' + colspan + '></td>';
863 }
864 calendarHeaderHtml += '<td class=\"wc-scrollbar-shim\"' + rowspan + '></td></tr>';
865
866 //users row
867 if (showAsSeparatedUser) {
868 calendarHeaderHtml += '<tr><td class=\"wc-time-column-header\"></td>';
869 var uLength = options.users.length,
870 _headerClass = '';
871
872 for (var i = 1; i <= options.daysToShow; i++) {
873 for (var j = 0; j < uLength; j++) {
874 _headerClass = [];
875 if (j == 0) {
876 _headerClass.push('wc-day-column-first');
877 }
878 if (j == uLength - 1) {
879 _headerClass.push('wc-day-column-last');
880 }
881 if (!_headerClass.length) {
882 _headerClass = 'wc-day-column-middle';
883 }
884 else {
885 _headerClass = _headerClass.join(' ');
886 }
887 calendarHeaderHtml += '<td class=\"' + _headerClass + ' wc-user-header wc-day-' + i + ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
888// calendarHeaderHtml+= "<div class=\"wc-user-header wc-day-" + i + " wc-user-" + self._getUserIdFromIndex(j) +"\" >";
889 calendarHeaderHtml += self._getUserName(j);
890// calendarHeaderHtml+= "</div>";
891 calendarHeaderHtml += '</td>';
892 }
893 }
894 calendarHeaderHtml += '</tr>';
895 }
896 //close the header
897 calendarHeaderHtml += '</tbody></table></div>';
898
899 $(calendarHeaderHtml).appendTo($calendarContainer);
900 },
901
902 /**
903 * render the calendar body.
904 * Calendar body is composed of several distinct parts.
905 * Each part is displayed in a separated row to ease rendering.
906 * for further explanations, see each part rendering function.
907 */
908 _renderCalendarBody: function($calendarContainer) {
909 var self = this, options = this.options,
910 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
911 $calendarBody, $calendarTableTbody;
912 // create the structure
913 $calendarBody = '<div class=\"wc-scrollable-grid\">';
914 $calendarBody += '<table class=\"wc-time-slots\">';
915 $calendarBody += '<tbody>';
916 $calendarBody += '</tbody>';
917 $calendarBody += '</table>';
918 $calendarBody += '</div>';
919 $calendarBody = $($calendarBody);
920 $calendarTableTbody = $calendarBody.find('tbody');
921
922 self._renderCalendarBodyTimeSlots($calendarTableTbody);
923 self._renderCalendarBodyOddEven($calendarTableTbody);
924 self._renderCalendarBodyFreeBusy($calendarTableTbody);
925 self._renderCalendarBodyEvents($calendarTableTbody);
926
927 $calendarBody.appendTo($calendarContainer);
928
929 //set the column height
930 $calendarContainer.find('.wc-full-height-column').height(options.timeslotHeight * options.timeslotsPerDay);
931 //set the timeslot height
932 $calendarContainer.find('.wc-time-slot').height(options.timeslotHeight - 1); //account for border
933 //init the time row header height
934 /**
935 TODO if total height for an hour is less than 11px, there is a display problem.
936 Find a way to handle it
937 */
938 $calendarContainer.find('.wc-time-header-cell').css({
939 height: (options.timeslotHeight * options.timeslotsPerHour) - 11,
940 padding: 5
941 });
942 //add the user data to every impacted column
943 if (showAsSeparatedUser) {
944 for (var i = 0, uLength = options.users.length; i < uLength; i++) {
945 $calendarContainer.find('.wc-user-' + self._getUserIdFromIndex(i))
946 .data('wcUser', options.users[i])
947 .data('wcUserIndex', i)
948 .data('wcUserId', self._getUserIdFromIndex(i));
949 }
950 }
951 },
952
953 /**
954 * render the timeslots separation
955 */
956 _renderCalendarBodyTimeSlots: function($calendarTableTbody) {
957 var options = this.options,
958 renderRow, i, j,
959 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
960 start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
961 end = (options.businessHours.limitDisplay ? options.businessHours.end : 24),
962 rowspan = 1;
963
964 //calculate the rowspan
965 if (options.displayOddEven) { rowspan += 1; }
966 if (options.displayFreeBusys) { rowspan += 1; }
967 if (rowspan > 1) {
968 rowspan = ' rowspan=\"' + rowspan + '\"';
969 }
970 else {
971 rowspan = '';
972 }
973
974 renderRow = '<tr class=\"wc-grid-row-timeslot\">';
975 renderRow += '<td class=\"wc-grid-timeslot-header\"' + rowspan + '></td>';
976 renderRow += '<td colspan=\"' + options.daysToShow * (showAsSeparatedUser ? options.users.length : 1) + '\">';
977 renderRow += '<div class=\"wc-no-height-wrapper wc-time-slot-wrapper\">';
978 renderRow += '<div class=\"wc-time-slots\">';
979
980 for (i = start; i < end; i++) {
981 for (j = 0; j < options.timeslotsPerHour - 1; j++) {
982 renderRow += '<div class=\"wc-time-slot\"></div>';
983 }
984 renderRow += '<div class=\"wc-time-slot wc-hour-end\"></div>';
985 }
986
987 renderRow += '</div>';
988 renderRow += '</div>';
989 renderRow += '</td>';
990 renderRow += '</tr>';
991
992 $(renderRow).appendTo($calendarTableTbody);
993 },
994
995 /**
996 * render the odd even columns
997 */
998 _renderCalendarBodyOddEven: function($calendarTableTbody) {
999 if (this.options.displayOddEven) {
1000 var options = this.options,
1001 renderRow = '<tr class=\"wc-grid-row-oddeven\">',
1002 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
1003 oddEven,
1004 // let's take advantage of the jquery ui framework
1005 oddEvenClasses = {'odd': 'wc-column-odd', 'even': 'ui-state-hover wc-column-even'};
1006
1007 //now let's display oddEven placeholders
1008 for (var i = 1; i <= options.daysToShow; i++) {
1009 if (!showAsSeparatedUser) {
1010 oddEven = (oddEven == 'odd' ? 'even' : 'odd');
1011 renderRow += '<td class=\"wc-day-column day-' + i + '\">';
1012 renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
1013 renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\"></div>';
1014 renderRow += '</div>';
1015 renderRow += '</td>';
1016 }
1017 else {
1018 var uLength = options.users.length;
1019 for (var j = 0; j < uLength; j++) {
1020 oddEven = (oddEven == 'odd' ? 'even' : 'odd');
1021 renderRow += '<td class=\"wc-day-column day-' + i + '\">';
1022 renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
1023 renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\" ></div>';
1024 renderRow += '</div>';
1025 renderRow += '</td>';
1026 }
1027 }
1028 }
1029 renderRow += '</tr>';
1030
1031 $(renderRow).appendTo($calendarTableTbody);
1032 }
1033 },
1034
1035 /**
1036 * render the freebusy placeholders
1037 */
1038 _renderCalendarBodyFreeBusy: function($calendarTableTbody) {
1039 if (this.options.displayFreeBusys) {
1040 var self = this, options = this.options,
1041 renderRow = '<tr class=\"wc-grid-row-freebusy\">',
1042 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
1043 renderRow += '</td>';
1044
1045 //now let's display freebusy placeholders
1046 for (var i = 1; i <= options.daysToShow; i++) {
1047 if (options.displayFreeBusys) {
1048 if (!showAsSeparatedUser) {
1049 renderRow += '<td class=\"wc-day-column day-' + i + '\">';
1050 renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
1051 renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i + '\"></div>';
1052 renderRow += '</div>';
1053 renderRow += '</td>';
1054 }
1055 else {
1056 var uLength = options.users.length;
1057 for (var j = 0; j < uLength; j++) {
1058 renderRow += '<td class=\"wc-day-column day-' + i + '\">';
1059 renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
1060 renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i;
1061 renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
1062 renderRow += '</div>';
1063 renderRow += '</div>';
1064 renderRow += '</td>';
1065 }
1066 }
1067 }
1068 }
1069
1070 renderRow += '</tr>';
1071
1072 $(renderRow).appendTo($calendarTableTbody);
1073 }
1074 },
1075
1076 /**
1077 * render the calendar body for event placeholders
1078 */
1079 _renderCalendarBodyEvents: function($calendarTableTbody) {
1080 var self = this, options = this.options,
1081 renderRow,
1082 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
1083 start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
1084 end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
1085 renderRow = '<tr class=\"wc-grid-row-events\">';
1086 renderRow += '<td class=\"wc-grid-timeslot-header\">';
1087 for (var i = start; i < end; i++) {
1088 var bhClass = (options.businessHours.start <= i && options.businessHours.end > i) ? 'ui-state-active wc-business-hours' : 'ui-state-default';
1089 renderRow += '<div class=\"wc-hour-header ' + bhClass + '\">';
1090 if (options.use24Hour) {
1091 renderRow += '<div class=\"wc-time-header-cell\">' + self._24HourForIndex(i) + '</div>';
1092 }
1093 else {
1094 renderRow += '<div class=\"wc-time-header-cell\">' + self._hourForIndex(i) + '<span class=\"wc-am-pm\">' + self._amOrPm(i) + '</span></div>';
1095 }
1096 renderRow += '</div>';
1097 }
1098 renderRow += '</td>';
1099
1100 //now let's display events placeholders
1101 var _columnBaseClass = 'ui-state-default wc-day-column';
1102 for (var i = 1; i <= options.daysToShow; i++) {
1103 if (!showAsSeparatedUser) {
1104 renderRow += '<td class=\"' + _columnBaseClass + ' wc-day-column-first wc-day-column-last day-' + i + '\">';
1105 renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i + '\"></div>';
1106 renderRow += '</td>';
1107 }
1108 else {
1109 var uLength = options.users.length;
1110 var columnclass;
1111 for (var j = 0; j < uLength; j++) {
1112 columnclass = [];
1113 if (j == 0) {
1114 columnclass.push('wc-day-column-first');
1115 }
1116 if (j == uLength - 1) {
1117 columnclass.push('wc-day-column-last');
1118 }
1119 if (!columnclass.length) {
1120 columnclass = 'wc-day-column-middle';
1121 }
1122 else {
1123 columnclass = columnclass.join(' ');
1124 }
1125 renderRow += '<td class=\"' + _columnBaseClass + ' ' + columnclass + ' day-' + i + '\">';
1126 renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i;
1127 renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
1128 renderRow += '</div>';
1129 renderRow += '</td>';
1130 }
1131 }
1132 }
1133
1134 renderRow += '</tr>';
1135
1136 $(renderRow).appendTo($calendarTableTbody);
1137 },
1138
1139 /*
1140 * setup mouse events for capturing new events
1141 */
1142 _setupEventCreationForWeekDay: function($weekDay) {
1143 var self = this;
1144 var options = this.options;
1145 $weekDay.mousedown(function(event) {
1146 var $target = $(event.target);
1147 if ($target.hasClass('wc-day-column-inner')) {
1148
1149 var $newEvent = $('<div class=\"wc-cal-event wc-new-cal-event wc-new-cal-event-creating\"></div>');
1150
1151 $newEvent.css({lineHeight: (options.timeslotHeight - 2) + 'px', fontSize: (options.timeslotHeight / 2) + 'px'});
1152 $target.append($newEvent);
1153
1154 var columnOffset = $target.offset().top;
1155 var clickY = event.pageY - columnOffset;
1156 var clickYRounded = (clickY - (clickY % options.timeslotHeight)) / options.timeslotHeight;
1157 var topPosition = clickYRounded * options.timeslotHeight;
1158 $newEvent.css({top: topPosition});
1159
1160 if (!options.preventDragOnEventCreation) {
1161 $target.bind('mousemove.newevent', function(event) {
1162 $newEvent.show();
1163 $newEvent.addClass('ui-resizable-resizing');
1164 var height = Math.round(event.pageY - columnOffset - topPosition);
1165 var remainder = height % options.timeslotHeight;
1166 //snap to closest timeslot
1167 if (remainder < 0) {
1168 var useHeight = height - remainder;
1169 $newEvent.css('height', useHeight < options.timeslotHeight ? options.timeslotHeight : useHeight);
1170 } else {
1171 $newEvent.css('height', height + (options.timeslotHeight - remainder));
1172 }
1173 }).mouseup(function() {
1174 $target.unbind('mousemove.newevent');
1175 $newEvent.addClass('ui-corner-all');
1176 });
1177 }
1178 }
1179
1180 }).mouseup(function(event) {
1181 var $target = $(event.target);
1182
1183 var $weekDay = $target.closest('.wc-day-column-inner');
1184 var $newEvent = $weekDay.find('.wc-new-cal-event-creating');
1185
1186 if ($newEvent.length) {
1187 var createdFromSingleClick = !$newEvent.hasClass('ui-resizable-resizing');
1188
1189 //if even created from a single click only, default height
1190 if (createdFromSingleClick) {
1191 $newEvent.css({height: options.timeslotHeight * options.defaultEventLength}).show();
1192 }
1193 var top = parseInt($newEvent.css('top'));
1194 var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $newEvent, top);
1195
1196 $newEvent.remove();
1197 var newCalEvent = {start: eventDuration.start, end: eventDuration.end, title: options.newEventText};
1198 var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
1199
1200 if (showAsSeparatedUser) {
1201 newCalEvent = self._setEventUserId(newCalEvent, $weekDay.data('wcUserId'));
1202 }
1203 else if (!options.showAsSeparateUsers && options.users && options.users.length == 1) {
1204 newCalEvent = self._setEventUserId(newCalEvent, self._getUserIdFromIndex(0));
1205 }
1206
1207 var freeBusyManager = self.getFreeBusyManagerForEvent(newCalEvent);
1208
1209 var $renderedCalEvent = self._renderEvent(newCalEvent, $weekDay);
1210
1211 if (!options.allowCalEventOverlap) {
1212 self._adjustForEventCollisions($weekDay, $renderedCalEvent, newCalEvent, newCalEvent);
1213 self._positionEvent($weekDay, $renderedCalEvent);
1214 } else {
1215 self._adjustOverlappingEvents($weekDay);
1216 }
1217
1218 var proceed = self._trigger('beforeEventNew', event, {
1219 'calEvent': newCalEvent,
1220 'createdFromSingleClick': createdFromSingleClick,
1221 'calendar': self.element
1222 });
1223 if (proceed) {
1224 options.eventNew(newCalEvent, $renderedCalEvent, freeBusyManager, self.element, event);
1225 }
1226 else {
1227 $($renderedCalEvent).remove();
1228 }
1229 }
1230 });
1231 },
1232
1233 /*
1234 * load calendar events for the week based on the date provided
1235 */
1236 _loadCalEvents: function(dateWithinWeek) {
1237
1238 var date, weekStartDate, weekEndDate, $weekDayColumns;
1239 var self = this;
1240 var options = this.options;
1241 date = this._fixMinMaxDate(dateWithinWeek || options.date);
1242 // if date is not provided
1243 // or was not set
1244 // or is different than old one
1245 if ((!date || !date.getTime) ||
1246 (!options.date || !options.date.getTime) ||
1247 date.getTime() != options.date.getTime()
1248 ) {
1249 // trigger the changedate event
1250 this._trigger('changedate', this.element, date);
1251 }
1252 this.options.date = date;
1253 weekStartDate = self._dateFirstDayOfWeek(date);
1254 weekEndDate = self._dateLastMilliOfWeek(date);
1255
1256 options.calendarBeforeLoad(self.element);
1257
1258 self.element.data('startDate', weekStartDate);
1259 self.element.data('endDate', weekEndDate);
1260
1261 $weekDayColumns = self.element.find('.wc-day-column-inner');
1262
1263 self._updateDayColumnHeader($weekDayColumns);
1264
1265 //load events by chosen means
1266 if (typeof options.data == 'string') {
1267 if (options.loading) {
1268 options.loading(true);
1269 }
1270 if (_currentAjaxCall) {
1271 // first abort current request.
1272 if (!_jQuery14OrLower) {
1273 _currentAjaxCall.abort();
1274 } else {
1275 // due to the fact that jquery 1.4 does not detect a request was
1276 // aborted, we need to replace the onreadystatechange and
1277 // execute the "complete" callback.
1278 _currentAjaxCall.onreadystatechange = null;
1279 _currentAjaxCall.abort();
1280 _currentAjaxCall = null;
1281 if (options.loading) {
1282 options.loading(false);
1283 }
1284 }
1285 }
1286 var jsonOptions = self._getJsonOptions();
1287 jsonOptions[options.startParam || 'start'] = Math.round(weekStartDate.getTime() / 1000);
1288 jsonOptions[options.endParam || 'end'] = Math.round(weekEndDate.getTime() / 1000);
1289 _currentAjaxCall = $.ajax({
1290 url: options.data,
1291 data: jsonOptions,
1292 dataType: 'json',
1293 error: function(XMLHttpRequest, textStatus, errorThrown) {
1294 // only prevent error with jQuery 1.5
1295 // see issue #34. thanks to dapplebeforedawn
1296 // (https://github.com/themouette/jquery-week-calendar/issues#issue/34)
1297 // for 1.5+, aborted request mean errorThrown == 'abort'
1298 // for prior version it means !errorThrown && !XMLHttpRequest.status
1299 // fixes #55
1300 if (errorThrown != 'abort' && XMLHttpRequest.status != 0) {
1301 alert('unable to get data, error:' + textStatus);
1302 }
1303 },
1304 success: function(data) {
1305 self._renderEvents(data, $weekDayColumns);
1306 },
1307 complete: function() {
1308 _currentAjaxCall = null;
1309 if (options.loading) {
1310 options.loading(false);
1311 }
1312 }
1313 });
1314 }
1315 else if ($.isFunction(options.data)) {
1316 options.data(weekStartDate, weekEndDate,
1317 function(data) {
1318 self._renderEvents(data, $weekDayColumns);
1319 });
1320 }
1321 else if (options.data) {
1322 self._renderEvents(options.data, $weekDayColumns);
1323 }
1324
1325 self._disableTextSelect($weekDayColumns);
1326 },
1327
1328 /**
1329 * Draws a thin line which indicates the current time.
1330 */
1331 _drawCurrentHourLine: function() {
1332 var d = new Date(),
1333 options = this.options,
1334 businessHours = options.businessHours;
1335
1336 // first, we remove the old hourline if it exists
1337 $('.wc-hourline', this.element).remove();
1338
1339 // the line does not need to be displayed
1340 if (businessHours.limitDisplay && d.getHours() > businessHours.end) {
1341 return;
1342 }
1343
1344 // then we recreate it
1345 var paddingStart = businessHours.limitDisplay ? businessHours.start : 0;
1346 var nbHours = d.getHours() - paddingStart + d.getMinutes() / 60;
1347 var positionTop = nbHours * options.timeslotHeight * options.timeslotsPerHour;
1348 var lineWidth = $('.wc-scrollable-grid .wc-today', this.element).width() + 3;
1349
1350 $('.wc-scrollable-grid .wc-today', this.element).append(
1351 $('<div>', {
1352 'class': 'wc-hourline',
1353 style: 'top: ' + positionTop + 'px; width: ' + lineWidth + 'px'
1354 })
1355 );
1356 },
1357
1358 /*
1359 * update the display of each day column header based on the calendar week
1360 */
1361 _updateDayColumnHeader: function($weekDayColumns) {
1362 var self = this;
1363 var options = this.options;
1364 var currentDay = self._cloneDate(self.element.data('startDate'));
1365 var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
1366 var todayClass = 'ui-state-active wc-today';
1367
1368 self.element.find('.wc-header td.wc-day-column-header').each(function(i, val) {
1369 $(this).html(self._getHeaderDate(currentDay));
1370 if (self._isToday(currentDay)) {
1371 $(this).addClass(todayClass);
1372 } else {
1373 $(this).removeClass(todayClass);
1374 }
1375 currentDay = self._addDays(currentDay, 1);
1376
1377 });
1378
1379 currentDay = self._cloneDate(self.element.data('startDate'));
1380 if (showAsSeparatedUser)
1381 {
1382 self.element.find('.wc-header td.wc-user-header').each(function(i, val) {
1383 if (self._isToday(currentDay)) {
1384 $(this).addClass(todayClass);
1385 } else {
1386 $(this).removeClass(todayClass);
1387 }
1388 currentDay = ((i + 1) % options.users.length) ? currentDay : self._addDays(currentDay, 1);
1389 });
1390 }
1391
1392 currentDay = self._cloneDate(self.element.data('startDate'));
1393
1394 $weekDayColumns.each(function(i, val) {
1395
1396 $(this).data('startDate', self._cloneDate(currentDay));
1397 $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
1398 if (self._isToday(currentDay)) {
1399 $(this).parent()
1400 .addClass(todayClass)
1401 .removeClass('ui-state-default');
1402 } else {
1403 $(this).parent()
1404 .removeClass(todayClass)
1405 .addClass('ui-state-default');
1406 }
1407
1408 if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
1409 currentDay = self._addDays(currentDay, 1);
1410 }
1411 });
1412
1413 //now update the freeBusy placeholders
1414 if (options.displayFreeBusys) {
1415 currentDay = self._cloneDate(self.element.data('startDate'));
1416 self.element.find('.wc-grid-row-freebusy .wc-column-freebusy').each(function(i, val) {
1417 $(this).data('startDate', self._cloneDate(currentDay));
1418 $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
1419 if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
1420 currentDay = self._addDays(currentDay, 1);
1421 }
1422 });
1423 }
1424
1425 // now update the calendar title
1426 if (this.options.title) {
1427 var date = this.options.date,
1428 start = self._cloneDate(self.element.data('startDate')),
1429 end = self._dateLastDayOfWeek(new Date(this._cloneDate(self.element.data('endDate')).getTime() - (MILLIS_IN_DAY))),
1430 title = this._getCalendarTitle(),
1431 date_format = options.dateFormat;
1432
1433 // replace the placeholders contained in the title
1434 title = title.replace('%start%', self._formatDate(start, date_format));
1435 title = title.replace('%end%', self._formatDate(end, date_format));
1436 title = title.replace('%date%', self._formatDate(date, date_format));
1437
1438 $('.wc-toolbar .wc-title', self.element).html(title);
1439 }
1440 //self._clearFreeBusys();
1441 },
1442
1443 /**
1444 * Gets the calendar raw title.
1445 */
1446 _getCalendarTitle: function() {
1447 if ($.isFunction(this.options.title)) {
1448 return this.options.title(this.options.daysToShow);
1449 }
1450
1451 return this.options.title || '';
1452 },
1453
1454 /**
1455 * Render the events into the calendar
1456 */
1457 _renderEvents: function(data, $weekDayColumns) {
1458 var self = this;
1459 var options = this.options;
1460 var eventsToRender, nbRenderedEvents = 0;
1461
1462 if (data.options) {
1463 var updateLayout = false;
1464 // update options
1465 $.each(data.options, function(key, value) {
1466 if (value !== options[key]) {
1467 options[key] = value;
1468 updateLayout = updateLayout || $.ui.weekCalendar.updateLayoutOptions[key];
1469 }
1470 });
1471
1472 self._computeOptions();
1473
1474 if (updateLayout) {
1475 var hour = self._getCurrentScrollHour();
1476 self.element.empty();
1477 self._renderCalendar();
1478 $weekDayColumns = self.element.find('.wc-time-slots .wc-day-column-inner');
1479 self._updateDayColumnHeader($weekDayColumns);
1480 self._resizeCalendar();
1481 self._scrollToHour(hour, false);
1482 }
1483 }
1484 this._clearCalendar();
1485
1486 if ($.isArray(data)) {
1487 eventsToRender = self._cleanEvents(data);
1488 } else if (data.events) {
1489 eventsToRender = self._cleanEvents(data.events);
1490 self._renderFreeBusys(data);
1491 }
1492
1493 $.each(eventsToRender, function(i, calEvent) {
1494 // render a multi day event as various event :
1495 // thanks to http://github.com/fbeauchamp/jquery-week-calendar
1496 var initialStart = new Date(calEvent.start);
1497 var initialEnd = new Date(calEvent.end);
1498 var maxHour = self.options.businessHours.limitDisplay ? self.options.businessHours.end : 24;
1499 var minHour = self.options.businessHours.limitDisplay ? self.options.businessHours.start : 0;
1500 var start = new Date(initialStart);
1501 var startDate = self._formatDate(start, 'Ymd');
1502 var endDate = self._formatDate(initialEnd, 'Ymd');
1503 var $weekDay;
1504 var isMultiday = false;
1505
1506 while (startDate < endDate) {
1507 calEvent.start = start;
1508
1509 // end of this virual calEvent is set to the end of the day
1510 calEvent.end.setFullYear(start.getFullYear());
1511 calEvent.end.setDate(start.getDate());
1512 calEvent.end.setMonth(start.getMonth());
1513 calEvent.end.setHours(maxHour, 0, 0);
1514
1515 if (($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
1516 self._renderEvent(calEvent, $weekDay);
1517 nbRenderedEvents += 1;
1518 }
1519
1520 // start is set to the begin of the new day
1521 start.setDate(start.getDate() + 1);
1522 start.setHours(minHour, 0, 0);
1523
1524 startDate = self._formatDate(start, 'Ymd');
1525 isMultiday = true;
1526 }
1527
1528 if (start <= initialEnd) {
1529 calEvent.start = start;
1530 calEvent.end = initialEnd;
1531
1532 if (((isMultiday && calEvent.start.getTime() != calEvent.end.getTime()) || !isMultiday) && ($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
1533 self._renderEvent(calEvent, $weekDay);
1534 nbRenderedEvents += 1;
1535 }
1536 }
1537
1538 // put back the initial start date
1539 calEvent.start = initialStart;
1540 });
1541
1542 $weekDayColumns.each(function() {
1543 self._adjustOverlappingEvents($(this));
1544 });
1545
1546 options.calendarAfterLoad(self.element);
1547
1548 _hourLineTimeout && clearInterval(_hourLineTimeout);
1549
1550 if (options.hourLine) {
1551 self._drawCurrentHourLine();
1552
1553 _hourLineTimeout = setInterval(function() {
1554 self._drawCurrentHourLine();
1555 }, 60 * 1000); // redraw the line each minute
1556 }
1557
1558 !nbRenderedEvents && options.noEvents();
1559 },
1560
1561 /*
1562 * Render a specific event into the day provided. Assumes correct
1563 * day for calEvent date
1564 */
1565 _renderEvent: function(calEvent, $weekDay) {
1566 var self = this;
1567 var options = this.options;
1568 if (calEvent.start.getTime() > calEvent.end.getTime()) {
1569 return; // can't render a negative height
1570 }
1571
1572 var eventClass, eventHtml, $calEventList, $modifiedEvent;
1573
1574 eventClass = calEvent.id ? 'wc-cal-event' : 'wc-cal-event wc-new-cal-event';
1575 eventHtml = '<div class=\"' + eventClass + ' ui-corner-all\">';
1576 eventHtml += '<div class=\"wc-time ui-corner-top\"></div>';
1577 //title ////
1578 eventHtml += '<div class=\"wc-title\"></div><a href="alternative_venues">Alternative Venues</a></div>';
1579
1580
1581
1582 $weekDay.each(function() {
1583 var $calEvent = $(eventHtml);
1584 $modifiedEvent = options.eventRender(calEvent, $calEvent);
1585 $calEvent = $modifiedEvent ? $modifiedEvent.appendTo($(this)) : $calEvent.appendTo($(this));
1586 $calEvent.css({lineHeight: (options.textSize + 2) + 'px', fontSize: options.textSize + 'px'});
1587
1588 self._refreshEventDetails(calEvent, $calEvent);
1589 self._positionEvent($(this), $calEvent);
1590
1591 //add to event list
1592 if ($calEventList) {
1593 $calEventList = $calEventList.add($calEvent);
1594 }
1595 else {
1596 $calEventList = $calEvent;
1597 }
1598 });
1599 $calEventList.show();
1600
1601 if (!options.readonly && options.resizable(calEvent, $calEventList)) {
1602 self._addResizableToCalEvent(calEvent, $calEventList, $weekDay);
1603 }
1604 if (!options.readonly && options.draggable(calEvent, $calEventList)) {
1605 self._addDraggableToCalEvent(calEvent, $calEventList);
1606 }
1607 options.eventAfterRender(calEvent, $calEventList);
1608
1609 return $calEventList;
1610
1611 },
1612 addEvent: function() {
1613 return this._renderEvent.apply(this, arguments);
1614 },
1615
1616 _adjustOverlappingEvents: function($weekDay) {
1617 var self = this;
1618 if (self.options.allowCalEventOverlap) {
1619 var groupsList = self._groupOverlappingEventElements($weekDay);
1620 $.each(groupsList, function() {
1621 var curGroups = this;
1622 $.each(curGroups, function(groupIndex) {
1623 var curGroup = this;
1624
1625 // do we want events to be displayed as overlapping
1626 if (self.options.overlapEventsSeparate) {
1627 var newWidth = self.options.totalEventsWidthPercentInOneColumn / curGroups.length;
1628 var newLeft = groupIndex * newWidth;
1629 } else {
1630 // TODO what happens when the group has more than 10 elements
1631 var newWidth = self.options.totalEventsWidthPercentInOneColumn - ((curGroups.length - 1) * 10);
1632 var newLeft = groupIndex * 10;
1633 }
1634 $.each(curGroup, function() {
1635 // bring mouseovered event to the front
1636 if (!self.options.overlapEventsSeparate) {
1637 $(this).bind('mouseover.z-index', function() {
1638 var $elem = $(this);
1639 $.each(curGroup, function() {
1640 $(this).css({'z-index': '1'});
1641 });
1642 $elem.css({'z-index': '3'});
1643 });
1644 }
1645 $(this).css({width: newWidth + '%', left: newLeft + '%', right: 0});
1646 });
1647 });
1648 });
1649 }
1650 },
1651
1652
1653 /*
1654 * Find groups of overlapping events
1655 */
1656 _groupOverlappingEventElements: function($weekDay) {
1657 var $events = $weekDay.find('.wc-cal-event:visible');
1658 var sortedEvents = $events.sort(function(a, b) {
1659 return $(a).data('calEvent').start.getTime() - $(b).data('calEvent').start.getTime();
1660 });
1661
1662 var lastEndTime = new Date(0, 0, 0);
1663 var groups = [];
1664 var curGroups = [];
1665 var $curEvent;
1666 $.each(sortedEvents, function() {
1667 $curEvent = $(this);
1668 //checks, if the current group list is not empty, if the overlapping is finished
1669 if (curGroups.length > 0) {
1670 if (lastEndTime.getTime() <= $curEvent.data('calEvent').start.getTime()) {
1671 //finishes the current group list by adding it to the resulting list of groups and cleans it
1672
1673 groups.push(curGroups);
1674 curGroups = [];
1675 }
1676 }
1677
1678 //finds the first group to fill with the event
1679 for (var groupIndex = 0; groupIndex < curGroups.length; groupIndex++) {
1680 if (curGroups[groupIndex].length > 0) {
1681 //checks if the event starts after the end of the last event of the group
1682 if (curGroups[groupIndex][curGroups[groupIndex].length - 1].data('calEvent').end.getTime() <= $curEvent.data('calEvent').start.getTime()) {
1683 curGroups[groupIndex].push($curEvent);
1684 if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
1685 lastEndTime = $curEvent.data('calEvent').end;
1686 }
1687 return;
1688 }
1689 }
1690 }
1691 //if not found, creates a new group
1692 curGroups.push([$curEvent]);
1693 if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
1694 lastEndTime = $curEvent.data('calEvent').end;
1695 }
1696 });
1697 //adds the last groups in result
1698 if (curGroups.length > 0) {
1699 groups.push(curGroups);
1700 }
1701 return groups;
1702 },
1703
1704
1705 /*
1706 * find the weekday in the current calendar that the calEvent falls within
1707 */
1708 _findWeekDayForEvent: function(calEvent, $weekDayColumns) {
1709
1710 var $weekDay,
1711 options = this.options,
1712 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
1713 user_ids = this._getEventUserId(calEvent);
1714
1715 if (!$.isArray(user_ids)) {
1716 user_ids = [user_ids];
1717 }
1718
1719 $weekDayColumns.each(function(index, curDay) {
1720 if ($(this).data('startDate').getTime() <= calEvent.start.getTime() &&
1721 $(this).data('endDate').getTime() >= calEvent.end.getTime() &&
1722 (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), user_ids) !== -1)
1723 ) {
1724 if ($weekDay) {
1725 $weekDay = $weekDay.add($(curDay));
1726 }
1727 else {
1728 $weekDay = $(curDay);
1729 }
1730 }
1731 });
1732
1733 return $weekDay;
1734 },
1735
1736 /*
1737 * update the events rendering in the calendar. Add if does not yet exist.
1738 */
1739 _updateEventInCalendar: function(calEvent) {
1740 var self = this;
1741 self._cleanEvent(calEvent);
1742
1743 if (calEvent.id) {
1744 self.element.find('.wc-cal-event').each(function() {
1745 if ($(this).data('calEvent').id === calEvent.id || $(this).hasClass('wc-new-cal-event')) {
1746 $(this).remove();
1747 // return false;
1748 }
1749 });
1750 }
1751
1752 var $weekDays = self._findWeekDayForEvent(calEvent, self.element.find('.wc-grid-row-events .wc-day-column-inner'));
1753 if ($weekDays) {
1754 $weekDays.each(function(index, weekDay) {
1755 var $weekDay = $(weekDay);
1756 var $calEvent = self._renderEvent(calEvent, $weekDay);
1757 self._adjustForEventCollisions($weekDay, $calEvent, calEvent, calEvent);
1758 self._refreshEventDetails(calEvent, $calEvent);
1759 self._positionEvent($weekDay, $calEvent);
1760 self._adjustOverlappingEvents($weekDay);
1761 });
1762 }
1763 },
1764
1765 /*
1766 * Position the event element within the weekday based on it's start / end dates.
1767 */
1768 _positionEvent: function($weekDay, $calEvent) {
1769 var options = this.options;
1770 var calEvent = $calEvent.data('calEvent');
1771 var pxPerMillis = $weekDay.height() / options.millisToDisplay;
1772 var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
1773 var startMillis = this._getDSTdayShift(calEvent.start).getTime() - this._getDSTdayShift(new Date(calEvent.start.getFullYear(), calEvent.start.getMonth(), calEvent.start.getDate(), firstHourDisplayed)).getTime();
1774 var eventMillis = this._getDSTdayShift(calEvent.end).getTime() - this._getDSTdayShift(calEvent.start).getTime();
1775 var pxTop = pxPerMillis * startMillis;
1776 var pxHeight = pxPerMillis * eventMillis;
1777 //var pxHeightFallback = pxPerMillis * (60 / options.timeslotsPerHour) * 60 * 1000;
1778 $calEvent.css({top: pxTop, height: pxHeight || (pxPerMillis * 3600000 / options.timeslotsPerHour)});
1779 },
1780
1781 /*
1782 * Determine the actual start and end times of a calevent based on it's
1783 * relative position within the weekday column and the starting hour of the
1784 * displayed calendar.
1785 */
1786 _getEventDurationFromPositionedEventElement: function($weekDay, $calEvent, top) {
1787 var options = this.options;
1788 var startOffsetMillis = options.businessHours.limitDisplay ? options.businessHours.start * 3600000 : 0;
1789 var start = new Date($weekDay.data('startDate').getTime() + startOffsetMillis + Math.round(top / options.timeslotHeight) * options.millisPerTimeslot);
1790 var end = new Date(start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot);
1791 return {start: this._getDSTdayShift(start, -1), end: this._getDSTdayShift(end, -1)};
1792 },
1793
1794 /*
1795 * If the calendar does not allow event overlap, adjust the start or end date if necessary to
1796 * avoid overlapping of events. Typically, shortens the resized / dropped event to it's max possible
1797 * duration based on the overlap. If no satisfactory adjustment can be made, the event is reverted to
1798 * it's original location.
1799 */
1800 _adjustForEventCollisions: function($weekDay, $calEvent, newCalEvent, oldCalEvent, maintainEventDuration) {
1801 var options = this.options;
1802
1803 if (options.allowCalEventOverlap) {
1804 return;
1805 }
1806 var adjustedStart, adjustedEnd;
1807 var self = this;
1808
1809 $weekDay.find('.wc-cal-event').not($calEvent).each(function() {
1810 var currentCalEvent = $(this).data('calEvent');
1811
1812 //has been dropped onto existing event overlapping the end time
1813 if (newCalEvent.start.getTime() < currentCalEvent.end.getTime() &&
1814 newCalEvent.end.getTime() >= currentCalEvent.end.getTime()) {
1815
1816 adjustedStart = currentCalEvent.end;
1817 }
1818
1819
1820 //has been dropped onto existing event overlapping the start time
1821 if (newCalEvent.end.getTime() > currentCalEvent.start.getTime() &&
1822 newCalEvent.start.getTime() <= currentCalEvent.start.getTime()) {
1823
1824 adjustedEnd = currentCalEvent.start;
1825 }
1826 //has been dropped inside existing event with same or larger duration
1827 if (oldCalEvent.resizable == false ||
1828 (newCalEvent.end.getTime() <= currentCalEvent.end.getTime() &&
1829 newCalEvent.start.getTime() >= currentCalEvent.start.getTime())) {
1830
1831 adjustedStart = oldCalEvent.start;
1832 adjustedEnd = oldCalEvent.end;
1833 return false;
1834 }
1835
1836 });
1837
1838
1839 newCalEvent.start = adjustedStart || newCalEvent.start;
1840
1841 if (adjustedStart && maintainEventDuration) {
1842 newCalEvent.end = new Date(adjustedStart.getTime() + (oldCalEvent.end.getTime() - oldCalEvent.start.getTime()));
1843 self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, oldCalEvent);
1844 } else {
1845 newCalEvent.end = adjustedEnd || newCalEvent.end;
1846 }
1847
1848
1849 //reset if new cal event has been forced to zero size
1850 if (newCalEvent.start.getTime() >= newCalEvent.end.getTime()) {
1851 newCalEvent.start = oldCalEvent.start;
1852 newCalEvent.end = oldCalEvent.end;
1853 }
1854
1855 $calEvent.data('calEvent', newCalEvent);
1856 },
1857
1858 /**
1859 * Add draggable capabilities to an event
1860 */
1861 _addDraggableToCalEvent: function(calEvent, $calEvent) {
1862 var options = this.options;
1863
1864 $calEvent.draggable({
1865 handle: '.wc-time',
1866 containment: 'div.wc-time-slots',
1867 snap: '.wc-day-column-inner',
1868 snapMode: 'inner',
1869 snapTolerance: options.timeslotHeight - 1,
1870 revert: 'invalid',
1871 opacity: 0.5,
1872 grid: [$calEvent.outerWidth() + 1, options.timeslotHeight],
1873 start: function(event, ui) {
1874 var $calEvent = ui.draggable || ui.helper;
1875 options.eventDrag(calEvent, $calEvent);
1876 }
1877 });
1878 },
1879
1880 /*
1881 * Add droppable capabilites to weekdays to allow dropping of calEvents only
1882 */
1883 _addDroppableToWeekDay: function($weekDay) {
1884 var self = this;
1885 var options = this.options;
1886 $weekDay.droppable({
1887 accept: '.wc-cal-event',
1888 drop: function(event, ui) {
1889 var $calEvent = ui.draggable;
1890 var top = Math.round(parseInt(ui.position.top));
1891 var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top);
1892 var calEvent = $calEvent.data('calEvent');
1893 var newCalEvent = $.extend(true, {}, calEvent, {start: eventDuration.start, end: eventDuration.end});
1894 var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
1895 if (showAsSeparatedUser) {
1896 // we may have dragged the event on column with a new user.
1897 // nice way to handle that is:
1898 // - get the newly dragged on user
1899 // - check if user is part of the event
1900 // - if yes, nothing changes, if not, find the old owner to remove it and add new one
1901 var newUserId = $weekDay.data('wcUserId');
1902 var userIdList = self._getEventUserId(calEvent);
1903 var oldUserId = $(ui.draggable.parents('.wc-day-column-inner').get(0)).data('wcUserId');
1904 if (!$.isArray(userIdList)) {
1905 userIdList = [userIdList];
1906 }
1907 if ($.inArray(newUserId, userIdList) == -1) {
1908 // remove old user
1909 var _index = $.inArray(oldUserId, userIdList);
1910 userIdList.splice(_index, 1);
1911 // add new user ?
1912 if ($.inArray(newUserId, userIdList) == -1) {
1913 userIdList.push(newUserId);
1914 }
1915 }
1916 newCalEvent = self._setEventUserId(newCalEvent, ((userIdList.length == 1) ? userIdList[0] : userIdList));
1917 }
1918 self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true);
1919 var $weekDayColumns = self.element.find('.wc-day-column-inner');
1920
1921 //trigger drop callback
1922 options.eventDrop(newCalEvent, calEvent, $calEvent);
1923
1924 var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns));
1925 $calEvent.hide();
1926
1927 $calEvent.data('preventClick', true);
1928
1929 var $weekDayOld = self._findWeekDayForEvent($calEvent.data('calEvent'), self.element.find('.wc-time-slots .wc-day-column-inner'));
1930
1931 if ($weekDayOld.data('startDate') != $weekDay.data('startDate')) {
1932 self._adjustOverlappingEvents($weekDayOld);
1933 }
1934 self._adjustOverlappingEvents($weekDay);
1935
1936 setTimeout(function() {
1937 $calEvent.remove();
1938 }, 1000);
1939
1940 }
1941 });
1942 },
1943
1944 /*
1945 * Add resizable capabilities to a calEvent
1946 */
1947 _addResizableToCalEvent: function(calEvent, $calEvent, $weekDay) {
1948 var self = this;
1949 var options = this.options;
1950 $calEvent.resizable({
1951 grid: options.timeslotHeight,
1952 containment: $weekDay,
1953 handles: 's',
1954 minHeight: options.timeslotHeight,
1955 stop: function(event, ui) {
1956 var $calEvent = ui.element;
1957 var newEnd = new Date($calEvent.data('calEvent').start.getTime() + Math.max(1, Math.round(ui.size.height / options.timeslotHeight)) * options.millisPerTimeslot);
1958 if (self._needDSTdayShift($calEvent.data('calEvent').start, newEnd))
1959 newEnd = self._getDSTdayShift(newEnd, -1);
1960 var newCalEvent = $.extend(true, {}, calEvent, {start: calEvent.start, end: newEnd});
1961 self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent);
1962
1963 //trigger resize callback
1964 options.eventResize(newCalEvent, calEvent, $calEvent);
1965 self._refreshEventDetails(newCalEvent, $calEvent);
1966 self._positionEvent($weekDay, $calEvent);
1967 self._adjustOverlappingEvents($weekDay);
1968 $calEvent.data('preventClick', true);
1969 setTimeout(function() {
1970 $calEvent.removeData('preventClick');
1971 }, 500);
1972 }
1973 });
1974 $('.ui-resizable-handle', $calEvent).text('=');
1975 },
1976
1977 /*
1978 * Refresh the displayed details of a calEvent in the calendar
1979 */
1980 _refreshEventDetails: function(calEvent, $calEvent) {
1981 var suffix = '';
1982 if (!this.options.readonly &&
1983 this.options.allowEventDelete &&
1984 this.options.deletable(calEvent,$calEvent)) {
1985 suffix = '<div class="wc-cal-event-delete ui-icon ui-icon-close"></div>';
1986 }
1987 $calEvent.find('.wc-time').html(this.options.eventHeader(calEvent, this.element) + suffix);
1988 $calEvent.find('.wc-title').html(this.options.eventBody(calEvent, this.element));
1989 $calEvent.data('calEvent', calEvent);
1990 this.options.eventRefresh(calEvent, $calEvent);
1991 },
1992
1993 /*
1994 * Clear all cal events from the calendar
1995 */
1996 _clearCalendar: function() {
1997 this.element.find('.wc-day-column-inner div').remove();
1998 this._clearFreeBusys();
1999 },
2000
2001 /*
2002 * Scroll the calendar to a specific hour
2003 */
2004 _scrollToHour: function(hour, animate) {
2005 var self = this;
2006 var options = this.options;
2007 var $scrollable = this.element.find('.wc-scrollable-grid');
2008 var slot = hour;
2009 if (self.options.businessHours.limitDisplay) {
2010 if (hour <= self.options.businessHours.start) {
2011 slot = 0;
2012 } else if (hour >= self.options.businessHours.end) {
2013 slot = self.options.businessHours.end - self.options.businessHours.start - 1;
2014 } else {
2015 slot = hour - self.options.businessHours.start;
2016 }
2017 }
2018
2019 var $target = this.element.find('.wc-grid-timeslot-header .wc-hour-header:eq(' + slot + ')');
2020
2021 $scrollable.animate({scrollTop: 0}, 0, function() {
2022 var targetOffset = $target.offset().top;
2023 var scroll = targetOffset - $scrollable.offset().top - $target.outerHeight();
2024 if (animate) {
2025 $scrollable.animate({scrollTop: scroll}, options.scrollToHourMillis);
2026 }
2027 else {
2028 $scrollable.animate({scrollTop: scroll}, 0);
2029 }
2030 });
2031 },
2032
2033 /*
2034 * find the hour (12 hour day) for a given hour index
2035 */
2036 _hourForIndex: function(index) {
2037 if (index === 0) { //midnight
2038 return 12;
2039 } else if (index < 13) { //am
2040 return index;
2041 } else { //pm
2042 return index - 12;
2043 }
2044 },
2045
2046 _24HourForIndex: function(index) {
2047 if (index === 0) { //midnight
2048 return '00:00';
2049 } else if (index < 10) {
2050 return '0' + index + ':00';
2051 } else {
2052 return index + ':00';
2053 }
2054 },
2055
2056 _amOrPm: function(hourOfDay) {
2057 return hourOfDay < 12 ? 'AM' : 'PM';
2058 },
2059
2060 _isToday: function(date) {
2061 var clonedDate = this._cloneDate(date);
2062 this._clearTime(clonedDate);
2063 var today = new Date();
2064 this._clearTime(today);
2065 return today.getTime() === clonedDate.getTime();
2066 },
2067
2068 /*
2069 * Clean events to ensure correct format
2070 */
2071 _cleanEvents: function(events) {
2072 var self = this;
2073 $.each(events, function(i, event) {
2074 self._cleanEvent(event);
2075 });
2076 return events;
2077 },
2078
2079 /*
2080 * Clean specific event
2081 */
2082 _cleanEvent: function(event) {
2083 if (event.date) {
2084 event.start = event.date;
2085 }
2086 event.start = this._cleanDate(event.start);
2087 event.end = this._cleanDate(event.end);
2088 if (!event.end) {
2089 event.end = this._addDays(this._cloneDate(event.start), 1);
2090 }
2091 },
2092
2093 /*
2094 * Disable text selection of the elements in different browsers
2095 */
2096 _disableTextSelect: function($elements) {
2097 $elements.each(function() {
2098 if ($.browser.mozilla) {//Firefox
2099 $(this).css('MozUserSelect', 'none');
2100 } else if ($.browser.msie) {//IE
2101 $(this).bind('selectstart', function() {
2102 return false;
2103 });
2104 } else {//Opera, etc.
2105 $(this).mousedown(function() {
2106 return false;
2107 });
2108 }
2109 });
2110 },
2111
2112 /*
2113 * returns the date on the first millisecond of the week
2114 */
2115 _dateFirstDayOfWeek: function(date) {
2116 var self = this;
2117 var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2118 var adjustedDate = new Date(midnightCurrentDate);
2119 adjustedDate.setDate(adjustedDate.getDate() - self._getAdjustedDayIndex(midnightCurrentDate));
2120
2121 return adjustedDate;
2122 },
2123
2124 /*
2125 * returns the date on the first millisecond of the last day of the week
2126 */
2127 _dateLastDayOfWeek: function(date) {
2128 var self = this;
2129 var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2130 var adjustedDate = new Date(midnightCurrentDate);
2131 var daysToAdd = (self.options.daysToShow - 1 - self._getAdjustedDayIndex(midnightCurrentDate));
2132 adjustedDate.setDate(adjustedDate.getDate() + daysToAdd);
2133
2134 return adjustedDate;
2135 },
2136
2137 /**
2138 * fix the date if it is not within given options
2139 * minDate and maxDate
2140 */
2141 _fixMinMaxDate: function(date) {
2142 var minDate, maxDate;
2143 date = this._cleanDate(date);
2144
2145 // not less than minDate
2146 if (this.options.minDate) {
2147 minDate = this._cleanDate(this.options.minDate);
2148 // midnight on minDate
2149 minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
2150 if (date.getTime() < minDate.getTime()) {
2151 this._trigger('reachedmindate', this.element, date);
2152 }
2153 date = this._cleanDate(Math.max(date.getTime(), minDate.getTime()));
2154 }
2155
2156 // not more than maxDate
2157 if (this.options.maxDate) {
2158 maxDate = this._cleanDate(this.options.maxDate);
2159 // apply correction for max date if not startOnFirstDayOfWeek
2160 // to make sure no further date is displayed.
2161 // otherwise, the complement will still be shown
2162 if (!this._startOnFirstDayOfWeek()) {
2163 var day = maxDate.getDate() - this.options.daysToShow + 1;
2164 maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), day);
2165 }
2166 // microsecond before midnight on maxDate
2167 maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999);
2168 if (date.getTime() > maxDate.getTime()) {
2169 this._trigger('reachedmaxdate', this.element, date);
2170 }
2171 date = this._cleanDate(Math.min(date.getTime(), maxDate.getTime()));
2172 }
2173
2174 return date;
2175 },
2176
2177 /*
2178 * gets the index of the current day adjusted based on options
2179 */
2180 _getAdjustedDayIndex: function(date) {
2181 if (!this._startOnFirstDayOfWeek()) {
2182 return 0;
2183 }
2184
2185 var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
2186 var currentDayOfStandardWeek = midnightCurrentDate.getDay();
2187 var days = [0, 1, 2, 3, 4, 5, 6];
2188 this._rotate(days, this._firstDayOfWeek());
2189 return days[currentDayOfStandardWeek];
2190 },
2191
2192 _firstDayOfWeek: function() {
2193 if ($.isFunction(this.options.firstDayOfWeek)) {
2194 return this.options.firstDayOfWeek(this.element);
2195 }
2196 return this.options.firstDayOfWeek;
2197 },
2198
2199 /*
2200 * returns the date on the last millisecond of the week
2201 */
2202 _dateLastMilliOfWeek: function(date) {
2203 var lastDayOfWeek = this._dateLastDayOfWeek(date);
2204 lastDayOfWeek = this._cloneDate(lastDayOfWeek);
2205 lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 1);
2206 return lastDayOfWeek;
2207
2208 },
2209
2210 /*
2211 * Clear the time components of a date leaving the date
2212 * of the first milli of day
2213 */
2214 _clearTime: function(d) {
2215 d.setHours(0);
2216 d.setMinutes(0);
2217 d.setSeconds(0);
2218 d.setMilliseconds(0);
2219 return d;
2220 },
2221
2222 /*
2223 * add specific number of days to date
2224 */
2225 _addDays: function(d, n, keepTime) {
2226 d.setDate(d.getDate() + n);
2227 if (keepTime) {
2228 return d;
2229 }
2230 return this._clearTime(d);
2231 },
2232
2233 /*
2234 * Rotate an array by specified number of places.
2235 */
2236 _rotate: function(a /*array*/, p /* integer, positive integer rotate to the right, negative to the left... */) {
2237 for (var l = a.length, p = (Math.abs(p) >= l && (p %= l), p < 0 && (p += l), p), i, x; p; p = (Math.ceil(l / p) - 1) * p - l + (l = p)) {
2238 for (i = l; i > p; x = a[--i], a[i] = a[i - p], a[i - p] = x) {}
2239 }
2240 return a;
2241 },
2242
2243 _cloneDate: function(d) {
2244 return new Date(d.getTime());
2245 },
2246
2247 /**
2248 * Return a Date instance for different representations.
2249 * Valid representations are:
2250 * * timestamps
2251 * * Date objects
2252 * * textual representations (only these accepted by the Date
2253 * constructor)
2254 *
2255 * @return {Date} The clean date object.
2256 */
2257 _cleanDate: function(d) {
2258 if (typeof d === 'string') {
2259 // if is numeric
2260 if (!isNaN(Number(d))) {
2261 return this._cleanDate(parseInt(d, 10));
2262 }
2263
2264 // this is a human readable date
2265 return Date.parse(d) || new Date(d);
2266 }
2267
2268 if (typeof d == 'number') {
2269 return new Date(d);
2270 }
2271
2272 return d;
2273 },
2274
2275 /*
2276 * date formatting is adapted from
2277 * http://jacwright.com/projects/javascript/date_format
2278 */
2279 _formatDate: function(date, format) {
2280 var returnStr = '';
2281 for (var i = 0; i < format.length; i++) {
2282 var curChar = format.charAt(i);
2283 if (i != 0 && format.charAt(i - 1) == '\\') {
2284 returnStr += curChar;
2285 }
2286 else if (this._replaceChars[curChar]) {
2287 returnStr += this._replaceChars[curChar](date, this);
2288 } else if (curChar != '\\') {
2289 returnStr += curChar;
2290 }
2291 }
2292 return returnStr;
2293 },
2294
2295 _replaceChars: {
2296 // Day
2297 d: function(date) { return (date.getDate() < 10 ? '0' : '') + date.getDate(); },
2298 D: function(date, calendar) { return calendar.options.shortDays[date.getDay()]; },
2299 j: function(date) { return date.getDate(); },
2300 l: function(date, calendar) { return calendar.options.longDays[date.getDay()]; },
2301 N: function(date) { var _d = date.getDay(); return _d ? _d : 7; },
2302 S: function(date) { return (date.getDate() % 10 == 1 && date.getDate() != 11 ? 'st' : (date.getDate() % 10 == 2 && date.getDate() != 12 ? 'nd' : (date.getDate() % 10 == 3 && date.getDate() != 13 ? 'rd' : 'th'))); },
2303 w: function(date) { return date.getDay(); },
2304 z: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((date - d) / 86400000); }, // Fixed now
2305 // Week
2306 W: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((((date - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
2307 // Month
2308 F: function(date, calendar) { return calendar.options.longMonths[date.getMonth()]; },
2309 m: function(date) { return (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1); },
2310 M: function(date, calendar) { return calendar.options.shortMonths[date.getMonth()]; },
2311 n: function(date) { return date.getMonth() + 1; },
2312 t: function(date) { var d = date; return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() }, // Fixed now, gets #days of date
2313 // Year
2314 L: function(date) { var year = date.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now
2315 o: function(date) { var d = new Date(date.valueOf()); d.setDate(d.getDate() - ((date.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
2316 Y: function(date) { return date.getFullYear(); },
2317 y: function(date) { return ('' + date.getFullYear()).substr(2); },
2318 // Time
2319 a: function(date) { return date.getHours() < 12 ? 'am' : 'pm'; },
2320 A: function(date) { return date.getHours() < 12 ? 'AM' : 'PM'; },
2321 B: function(date) { return Math.floor((((date.getUTCHours() + 1) % 24) + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
2322 g: function(date) { return date.getHours() % 12 || 12; },
2323 G: function(date) { return date.getHours(); },
2324 h: function(date) { return ((date.getHours() % 12 || 12) < 10 ? '0' : '') + (date.getHours() % 12 || 12); },
2325 H: function(date) { return (date.getHours() < 10 ? '0' : '') + date.getHours(); },
2326 i: function(date) { return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); },
2327 s: function(date) { return (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); },
2328 u: function(date) { var m = date.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
2329 // Timezone
2330 e: function(date) { return 'Not Yet Supported'; },
2331 I: function(date) { return 'Not Yet Supported'; },
2332 O: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + '00'; },
2333 P: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
2334 T: function(date) { var m = date.getMonth(); date.setMonth(0); var result = date.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); date.setMonth(m); return result;},
2335 Z: function(date) { return -date.getTimezoneOffset() * 60; },
2336 // Full Date/Time
2337 c: function(date, calendar) { return calendar._formatDate(date, 'Y-m-d\\TH:i:sP'); }, // Fixed now
2338 r: function(date, calendar) { return calendar._formatDate(date, 'D, d M Y H:i:s O'); },
2339 U: function(date) { return date.getTime() / 1000; }
2340 },
2341
2342 /* USER MANAGEMENT FUNCTIONS */
2343
2344 getUserForId: function(id) {
2345 return $.extend({}, this.options.users[this._getUserIndexFromId(id)]);
2346 },
2347
2348 /**
2349 * return the user name for header
2350 */
2351 _getUserName: function(index) {
2352 var self = this;
2353 var options = this.options;
2354 var user = options.users[index];
2355 if ($.isFunction(options.getUserName)) {
2356 return options.getUserName(user, index, self.element);
2357 }
2358 else {
2359 return user;
2360 }
2361 },
2362 /**
2363 * return the user id for given index
2364 */
2365 _getUserIdFromIndex: function(index) {
2366 var self = this;
2367 var options = this.options;
2368 if ($.isFunction(options.getUserId)) {
2369 return options.getUserId(options.users[index], index, self.element);
2370 }
2371 return index;
2372 },
2373 /**
2374 * returns the associated user index for given ID
2375 */
2376 _getUserIndexFromId: function(id) {
2377 var self = this;
2378 var options = this.options;
2379 for (var i = 0; i < options.users.length; i++) {
2380 if (self._getUserIdFromIndex(i) == id) {
2381 return i;
2382 }
2383 }
2384 return 0;
2385 },
2386 /**
2387 * return the user ids for given calEvent.
2388 * default is calEvent.userId field.
2389 */
2390 _getEventUserId: function(calEvent) {
2391 var self = this;
2392 var options = this.options;
2393 if (options.showAsSeparateUsers && options.users && options.users.length) {
2394 if ($.isFunction(options.getEventUserId)) {
2395 return options.getEventUserId(calEvent, self.element);
2396 }
2397 return calEvent.userId;
2398 }
2399 return [];
2400 },
2401 /**
2402 * sets the event user id on given calEvent
2403 * default is calEvent.userId field.
2404 */
2405 _setEventUserId: function(calEvent, userId) {
2406 var self = this;
2407 var options = this.options;
2408 if ($.isFunction(options.setEventUserId)) {
2409 return options.setEventUserId(userId, calEvent, self.element);
2410 }
2411 calEvent.userId = userId;
2412 return calEvent;
2413 },
2414 /**
2415 * return the user ids for given freeBusy.
2416 * default is freeBusy.userId field.
2417 */
2418 _getFreeBusyUserId: function(freeBusy) {
2419 var self = this;
2420 var options = this.options;
2421 if ($.isFunction(options.getFreeBusyUserId)) {
2422 return options.getFreeBusyUserId(freeBusy.getOption(), self.element);
2423 }
2424 return freeBusy.getOption('userId');
2425 },
2426
2427 /* FREEBUSY MANAGEMENT */
2428
2429 /**
2430 * ckean the free busy managers and remove all the freeBusy
2431 */
2432 _clearFreeBusys: function() {
2433 if (this.options.displayFreeBusys) {
2434 var self = this,
2435 options = this.options,
2436 $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
2437 $freeBusyPlaceholders.each(function() {
2438 $(this).data('wcFreeBusyManager', new FreeBusyManager({
2439 start: self._cloneDate($(this).data('startDate')),
2440 end: self._cloneDate($(this).data('endDate')),
2441 defaultFreeBusy: options.defaultFreeBusy || {}
2442 }));
2443 });
2444 self.element.find('.wc-grid-row-freebusy .wc-freebusy').remove();
2445 }
2446 },
2447 /**
2448 * retrieve placeholders for given freebusy
2449 */
2450 _findWeekDaysForFreeBusy: function(freeBusy, $weekDays) {
2451 var $returnWeekDays,
2452 options = this.options,
2453 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
2454 self = this,
2455 userList = self._getFreeBusyUserId(freeBusy);
2456 if (!$.isArray(userList)) {
2457 userList = userList != 'undefined' ? [userList] : [];
2458 }
2459 if (!$weekDays) {
2460 $weekDays = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
2461 }
2462 $weekDays.each(function() {
2463 var manager = $(this).data('wcFreeBusyManager'),
2464 has_overlap = manager.isWithin(freeBusy.getStart()) ||
2465 manager.isWithin(freeBusy.getEnd()) ||
2466 freeBusy.isWithin(manager.getStart()) ||
2467 freeBusy.isWithin(manager.getEnd()),
2468 userId = $(this).data('wcUserId');
2469 if (has_overlap && (!showAsSeparatedUser || ($.inArray(userId, userList) != -1))) {
2470 $returnWeekDays = $returnWeekDays ? $returnWeekDays.add($(this)) : $(this);
2471 }
2472 });
2473 return $returnWeekDays;
2474 },
2475
2476 /**
2477 * used to render all freeBusys
2478 */
2479 _renderFreeBusys: function(freeBusys) {
2480 if (this.options.displayFreeBusys) {
2481 var self = this,
2482 $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
2483 freebusysToRender;
2484 //insert freebusys to dedicated placeholders freebusy managers
2485 if ($.isArray(freeBusys)) {
2486 freebusysToRender = self._cleanFreeBusys(freeBusys);
2487 } else if (freeBusys.freebusys) {
2488 freebusysToRender = self._cleanFreeBusys(freeBusys.freebusys);
2489 }
2490 else {
2491 freebusysToRender = [];
2492 }
2493
2494 $.each(freebusysToRender, function(index, freebusy) {
2495 var $placeholders = self._findWeekDaysForFreeBusy(freebusy, $freeBusyPlaceholders);
2496 if ($placeholders) {
2497 $placeholders.each(function() {
2498 var manager = $(this).data('wcFreeBusyManager');
2499 manager.insertFreeBusy(new FreeBusy(freebusy.getOption()));
2500 $(this).data('wcFreeBusyManager', manager);
2501 });
2502 }
2503 });
2504
2505 //now display freebusys on place holders
2506 self._refreshFreeBusys($freeBusyPlaceholders);
2507 }
2508 },
2509 /**
2510 * refresh freebusys for given placeholders
2511 */
2512 _refreshFreeBusys: function($freeBusyPlaceholders) {
2513 if (this.options.displayFreeBusys && $freeBusyPlaceholders) {
2514 var self = this,
2515 options = this.options,
2516 start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
2517 end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
2518
2519 $freeBusyPlaceholders.each(function() {
2520 var $placehoder = $(this);
2521 var s = self._cloneDate($placehoder.data('startDate')),
2522 e = self._cloneDate(s);
2523 s.setHours(start);
2524 e.setHours(end);
2525 $placehoder.find('.wc-freebusy').remove();
2526 $.each($placehoder.data('wcFreeBusyManager').getFreeBusys(s, e), function() {
2527 self._renderFreeBusy(this, $placehoder);
2528 });
2529 });
2530 }
2531 },
2532 /**
2533 * render a freebusy item on dedicated placeholders
2534 */
2535 _renderFreeBusy: function(freeBusy, $freeBusyPlaceholder) {
2536 if (this.options.displayFreeBusys) {
2537 var self = this,
2538 options = this.options,
2539 freeBusyHtml = '<div class="wc-freebusy"></div>';
2540
2541 var $fb = $(freeBusyHtml);
2542 $fb.data('wcFreeBusy', new FreeBusy(freeBusy.getOption()));
2543 this._positionFreeBusy($freeBusyPlaceholder, $fb);
2544 $fb = options.freeBusyRender(freeBusy.getOption(), $fb, self.element);
2545 if ($fb) {
2546 $fb.appendTo($freeBusyPlaceholder);
2547 }
2548 }
2549 },
2550 /*
2551 * Position the freebusy element within the weekday based on it's start / end dates.
2552 */
2553 _positionFreeBusy: function($placeholder, $freeBusy) {
2554 var options = this.options;
2555 var freeBusy = $freeBusy.data('wcFreeBusy');
2556 var pxPerMillis = $placeholder.height() / options.millisToDisplay;
2557 var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
2558 var startMillis = freeBusy.getStart().getTime() - new Date(freeBusy.getStart().getFullYear(), freeBusy.getStart().getMonth(), freeBusy.getStart().getDate(), firstHourDisplayed).getTime();
2559 var eventMillis = freeBusy.getEnd().getTime() - freeBusy.getStart().getTime();
2560 var pxTop = pxPerMillis * startMillis;
2561 var pxHeight = pxPerMillis * eventMillis;
2562 $freeBusy.css({top: pxTop, height: pxHeight});
2563 },
2564 /*
2565 * Clean freebusys to ensure correct format
2566 */
2567 _cleanFreeBusys: function(freebusys) {
2568 var self = this,
2569 freeBusyToReturn = [];
2570 if (!$.isArray(freebusys)) {
2571 var freebusys = [freebusys];
2572 }
2573 $.each(freebusys, function(i, freebusy) {
2574 freeBusyToReturn.push(new FreeBusy(self._cleanFreeBusy(freebusy)));
2575 });
2576 return freeBusyToReturn;
2577 },
2578
2579 /*
2580 * Clean specific freebusy
2581 */
2582 _cleanFreeBusy: function(freebusy) {
2583 if (freebusy.date) {
2584 freebusy.start = freebusy.date;
2585 }
2586 freebusy.start = this._cleanDate(freebusy.start);
2587 freebusy.end = this._cleanDate(freebusy.end);
2588 return freebusy;
2589 },
2590
2591 /**
2592 * retrives the first freebusy manager matching demand.
2593 */
2594 getFreeBusyManagersFor: function(date, users) {
2595 var calEvent = {
2596 start: date,
2597 end: date
2598 };
2599 this._setEventUserId(calEvent, users);
2600 return this.getFreeBusyManagerForEvent(calEvent);
2601 },
2602 /**
2603 * retrives the first freebusy manager for given event.
2604 */
2605 getFreeBusyManagerForEvent: function(newCalEvent) {
2606 var self = this,
2607 options = this.options,
2608 freeBusyManager;
2609 if (options.displayFreeBusys) {
2610 var $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
2611 freeBusy = new FreeBusy({start: newCalEvent.start, end: newCalEvent.end}),
2612 showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
2613 userId = showAsSeparatedUser ? self._getEventUserId(newCalEvent) : null;
2614 if (!$.isArray(userId)) {
2615 userId = [userId];
2616 }
2617 $freeBusyPlaceHoders.each(function() {
2618 var manager = $(this).data('wcFreeBusyManager'),
2619 has_overlap = manager.isWithin(freeBusy.getEnd()) ||
2620 manager.isWithin(freeBusy.getEnd()) ||
2621 freeBusy.isWithin(manager.getStart()) ||
2622 freeBusy.isWithin(manager.getEnd());
2623 if (has_overlap && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), userId) != -1)) {
2624 freeBusyManager = $(this).data('wcFreeBusyManager');
2625 return false;
2626 }
2627 });
2628 }
2629 return freeBusyManager;
2630 },
2631 /**
2632 * appends the freebusys to replace the old ones.
2633 * @param {array|object} freeBusys freebusy(s) to apply.
2634 */
2635 updateFreeBusy: function(freeBusys) {
2636 var self = this,
2637 options = this.options;
2638 if (options.displayFreeBusys) {
2639 var $toRender,
2640 $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
2641 _freeBusys = self._cleanFreeBusys(freeBusys);
2642
2643 $.each(_freeBusys, function(index, _freeBusy) {
2644
2645 var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHoders);
2646 //if freebusy has a placeholder
2647 if ($weekdays && $weekdays.length) {
2648 $weekdays.each(function(index, day) {
2649 var manager = $(day).data('wcFreeBusyManager');
2650 manager.insertFreeBusy(_freeBusy);
2651 $(day).data('wcFreeBusyManager', manager);
2652 });
2653 $toRender = $toRender ? $toRender.add($weekdays) : $weekdays;
2654 }
2655 });
2656 self._refreshFreeBusys($toRender);
2657 }
2658 },
2659
2660 /* NEW OPTIONS MANAGEMENT */
2661
2662 /**
2663 * checks wether or not the calendar should be displayed starting on first day of week
2664 */
2665 _startOnFirstDayOfWeek: function() {
2666 return jQuery.isFunction(this.options.startOnFirstDayOfWeek) ? this.options.startOnFirstDayOfWeek(this.element) : this.options.startOnFirstDayOfWeek;
2667 },
2668
2669 /**
2670 * finds out the current scroll to apply it when changing the view
2671 */
2672 _getCurrentScrollHour: function() {
2673 var self = this;
2674 var options = this.options;
2675 var $scrollable = this.element.find('.wc-scrollable-grid');
2676 var scroll = $scrollable.scrollTop();
2677 if (self.options.businessHours.limitDisplay) {
2678 scroll = scroll + options.businessHours.start * options.timeslotHeight * options.timeslotsPerHour;
2679 }
2680 return Math.round(scroll / (options.timeslotHeight * options.timeslotsPerHour)) + 1;
2681 },
2682 _getJsonOptions: function() {
2683 if ($.isFunction(this.options.jsonOptions)) {
2684 return $.extend({}, this.options.jsonOptions(this.element));
2685 }
2686 if ($.isPlainObject(this.options.jsonOptions)) {
2687 return $.extend({}, this.options.jsonOptions);
2688 }
2689 return {};
2690 },
2691 _getHeaderDate: function(date) {
2692 var options = this.options;
2693 if (options.getHeaderDate && $.isFunction(options.getHeaderDate))
2694 {
2695 return options.getHeaderDate(date, this.element);
2696 }
2697 var dayName = options.useShortDayNames ? options.shortDays[date.getDay()] : options.longDays[date.getDay()];
2698 return dayName + (options.headerSeparator) + this._formatDate(date, options.dateFormat);
2699 },
2700
2701
2702
2703 /**
2704 * returns corrected date related to DST problem
2705 */
2706 _getDSTdayShift: function(date, shift) {
2707 var start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0);
2708 var offset1 = start.getTimezoneOffset();
2709 var offset2 = date.getTimezoneOffset();
2710 if (offset1 == offset2)
2711 return date;
2712 shift = shift ? shift : 1;
2713 return new Date(date.getTime() - shift * (offset1 > offset2 ? -1 : 1) * (Math.max(offset1, offset2) - Math.min(offset1, offset2)) * 60000);
2714 },
2715 _needDSTdayShift: function(date1, date2) {
2716 return date1.getTimezoneOffset() != date2.getTimezoneOffset();
2717 }
2718
2719
2720
2721 }; // end of widget function return
2722 })() //end of widget function closure execution
2723 ); // end of $.widget("ui.weekCalendar"...
2724
2725 $.extend($.ui.weekCalendar, {
2726 version: '2.0-dev',
2727 updateLayoutOptions: {
2728 startOnFirstDayOfWeek: true,
2729 firstDayOfWeek: true,
2730 daysToShow: true,
2731 displayOddEven: true,
2732 timeFormat: true,
2733 dateFormat: true,
2734 use24Hour: true,
2735 useShortDayNames: true,
2736 businessHours: true,
2737 timeslotHeight: true,
2738 timeslotsPerHour: true,
2739 buttonText: true,
2740 height: true,
2741 shortMonths: true,
2742 longMonths: true,
2743 shortDays: true,
2744 longDays: true,
2745 textSize: true,
2746 users: true,
2747 showAsSeparateUsers: true,
2748 displayFreeBusys: true
2749 }
2750 });
2751
2752 var MILLIS_IN_DAY = 86400000;
2753 var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
2754
2755 /* FREE BUSY MANAGERS */
2756 var FreeBusyProto = {
2757 getStart: function() {return this.getOption('start')},
2758 getEnd: function() {return this.getOption('end')},
2759 getOption: function() {
2760 if (!arguments.length) { return this.options }
2761 if (typeof(this.options[arguments[0]]) !== 'undefined') {
2762 return this.options[arguments[0]];
2763 }
2764 else if (typeof(arguments[1]) !== 'undefined') {
2765 return arguments[1];
2766 }
2767 return null;
2768 },
2769 setOption: function(key, value) {
2770 if (arguments.length == 1) {
2771 $.extend(this.options, arguments[0]);
2772 return this;
2773 }
2774 this.options[key] = value;
2775 return this;
2776 },
2777 isWithin: function(dateTime) {return Math.floor(dateTime.getTime() / 1000) >= Math.floor(this.getStart().getTime() / 1000) && Math.floor(dateTime.getTime() / 1000) <= Math.floor(this.getEnd().getTime() / 1000)},
2778 isValid: function() {return this.getStart().getTime() < this.getEnd().getTime()}
2779 };
2780
2781 /**
2782 * @constructor
2783 * single user freebusy manager.
2784 */
2785 var FreeBusy = function(options) {
2786 this.options = $.extend({}, options || {});
2787 };
2788 $.extend(FreeBusy.prototype, FreeBusyProto);
2789
2790 var FreeBusyManager = function(options) {
2791 this.options = $.extend({
2792 defaultFreeBusy: {}
2793 }, options || {});
2794 this.freeBusys = [];
2795 this.freeBusys.push(new FreeBusy($.extend({
2796 start: this.getStart(),
2797 end: this.getEnd()
2798 }, this.options.defaultFreeBusy)));
2799 };
2800 $.extend(FreeBusyManager.prototype, FreeBusyProto, {
2801 /**
2802 * return matching freeBusys.
2803 * if you do not pass any argument, returns all freebusys.
2804 * if you only pass a start date, only matchinf freebusy will be returned.
2805 * if you pass 2 arguments, then all freebusys available within the time period will be returned
2806 * @param {Date} start [optionnal] if you do not pass end date, will return the freeBusy within which this date falls.
2807 * @param {Date} end [optionnal] the date where to stop the search.
2808 * @return {Array} an array of FreeBusy matching arguments.
2809 */
2810 getFreeBusys: function() {
2811 switch (arguments.length) {
2812 case 0:
2813 return this.freeBusys;
2814 case 1:
2815 var freeBusy = [];
2816 var start = arguments[0];
2817 if (!this.isWithin(start)) {
2818 return freeBusy;
2819 }
2820 $.each(this.freeBusys, function() {
2821 if (this.isWithin(start)) {
2822 freeBusy.push(this);
2823 }
2824 if (Math.floor(this.getEnd().getTime() / 1000) > Math.floor(start.getTime() / 1000)) {
2825 return false;
2826 }
2827 });
2828 return freeBusy;
2829 default:
2830 //we assume only 2 first args are revealants
2831 var freeBusy = [];
2832 var start = arguments[0], end = arguments[1];
2833 var tmpFreeBusy = new FreeBusy({start: start, end: end});
2834 if (end.getTime() < start.getTime() || this.getStart().getTime() > end.getTime() || this.getEnd().getTime() < start.getTime()) {
2835 return freeBusy;
2836 }
2837 $.each(this.freeBusys, function() {
2838 if (this.getStart().getTime() >= end.getTime()) {
2839 return false;
2840 }
2841 if (tmpFreeBusy.isWithin(this.getStart()) && tmpFreeBusy.isWithin(this.getEnd())) {
2842 freeBusy.push(this);
2843 }
2844 else if (this.isWithin(tmpFreeBusy.getStart()) && this.isWithin(tmpFreeBusy.getEnd())) {
2845 var _f = new FreeBusy(this.getOption());
2846 _f.setOption('end', tmpFreeBusy.getEnd());
2847 _f.setOption('start', tmpFreeBusy.getStart());
2848 freeBusy.push(_f);
2849 }
2850 else if (this.isWithin(tmpFreeBusy.getStart()) && this.getStart().getTime() < start.getTime()) {
2851 var _f = new FreeBusy(this.getOption());
2852 _f.setOption('start', tmpFreeBusy.getStart());
2853 freeBusy.push(_f);
2854 }
2855 else if (this.isWithin(tmpFreeBusy.getEnd()) && this.getEnd().getTime() > end.getTime()) {
2856 var _f = new FreeBusy(this.getOption());
2857 _f.setOption('end', tmpFreeBusy.getEnd());
2858 freeBusy.push(_f);
2859 }
2860 });
2861 return freeBusy;
2862 }
2863 },
2864 insertFreeBusy: function(freeBusy) {
2865 var freeBusy = new FreeBusy(freeBusy.getOption());
2866 //first, if inserted freebusy is bigger than manager
2867 if (freeBusy.getStart().getTime() < this.getStart().getTime()) {
2868 freeBusy.setOption('start', this.getStart());
2869 }
2870 if (freeBusy.getEnd().getTime() > this.getEnd().getTime()) {
2871 freeBusy.setOption('end', this.getEnd());
2872 }
2873 var start = freeBusy.getStart(), end = freeBusy.getEnd(),
2874 startIndex = 0, endIndex = this.freeBusys.length - 1,
2875 newFreeBusys = [];
2876 var pushNewFreeBusy = function(_f) {if (_f.isValid()) newFreeBusys.push(_f);};
2877
2878 $.each(this.freeBusys, function(index) {
2879 //within the loop, we have following vars:
2880 // curFreeBusyItem: the current iteration freeBusy, part of manager freeBusys list
2881 // start: the insterted freeBusy start
2882 // end: the inserted freebusy end
2883 var curFreeBusyItem = this;
2884 if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.isWithin(end)) {
2885 /*
2886 we are in case where inserted freebusy fits in curFreeBusyItem:
2887 curFreeBusyItem: *-----------------------------*
2888 freeBusy: *-------------*
2889 obviously, start and end indexes are this item.
2890 */
2891 startIndex = index;
2892 endIndex = index;
2893 if (start.getTime() == curFreeBusyItem.getStart().getTime() && end.getTime() == curFreeBusyItem.getEnd().getTime()) {
2894 /*
2895 in this case, inserted freebusy is exactly curFreeBusyItem:
2896 curFreeBusyItem: *-----------------------------*
2897 freeBusy: *-----------------------------*
2898
2899 just replace curFreeBusyItem with freeBusy.
2900 */
2901 var _f1 = new FreeBusy(freeBusy.getOption());
2902 pushNewFreeBusy(_f1);
2903 }
2904 else if (start.getTime() == curFreeBusyItem.getStart().getTime()) {
2905 /*
2906 in this case inserted freebusy starts with curFreeBusyItem:
2907 curFreeBusyItem: *-----------------------------*
2908 freeBusy: *--------------*
2909
2910 just replace curFreeBusyItem with freeBusy AND the rest.
2911 */
2912 var _f1 = new FreeBusy(freeBusy.getOption());
2913 var _f2 = new FreeBusy(curFreeBusyItem.getOption());
2914 _f2.setOption('start', end);
2915 pushNewFreeBusy(_f1);
2916 pushNewFreeBusy(_f2);
2917 }
2918 else if (end.getTime() == curFreeBusyItem.getEnd().getTime()) {
2919 /*
2920 in this case inserted freebusy ends with curFreeBusyItem:
2921 curFreeBusyItem: *-----------------------------*
2922 freeBusy: *--------------*
2923
2924 just replace curFreeBusyItem with before part AND freeBusy.
2925 */
2926 var _f1 = new FreeBusy(curFreeBusyItem.getOption());
2927 _f1.setOption('end', start);
2928 var _f2 = new FreeBusy(freeBusy.getOption());
2929 pushNewFreeBusy(_f1);
2930 pushNewFreeBusy(_f2);
2931 }
2932 else {
2933 /*
2934 in this case inserted freebusy is within curFreeBusyItem:
2935 curFreeBusyItem: *-----------------------------*
2936 freeBusy: *--------------*
2937
2938 just replace curFreeBusyItem with before part AND freeBusy AND the rest.
2939 */
2940 var _f1 = new FreeBusy(curFreeBusyItem.getOption());
2941 var _f2 = new FreeBusy(freeBusy.getOption());
2942 var _f3 = new FreeBusy(curFreeBusyItem.getOption());
2943 _f1.setOption('end', start);
2944 _f3.setOption('start', end);
2945 pushNewFreeBusy(_f1);
2946 pushNewFreeBusy(_f2);
2947 pushNewFreeBusy(_f3);
2948 }
2949 /*
2950 as work is done, no need to go further.
2951 return false
2952 */
2953 return false;
2954 }
2955 else if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.getEnd().getTime() != start.getTime()) {
2956 /*
2957 in this case, inserted freebusy starts within curFreeBusyItem:
2958 curFreeBusyItem: *----------*
2959 freeBusy: *-------------------*
2960
2961 set start index AND insert before part, we'll insert freebusy later
2962 */
2963 if (curFreeBusyItem.getStart().getTime() != start.getTime()) {
2964 var _f1 = new FreeBusy(curFreeBusyItem.getOption());
2965 _f1.setOption('end', start);
2966 pushNewFreeBusy(_f1);
2967 }
2968 startIndex = index;
2969 }
2970 else if (curFreeBusyItem.isWithin(end) && curFreeBusyItem.getStart().getTime() != end.getTime()) {
2971 /*
2972 in this case, inserted freebusy starts within curFreeBusyItem:
2973 curFreeBusyItem: *----------*
2974 freeBusy: *-------------------*
2975
2976 set end index AND insert freebusy AND insert after part if needed
2977 */
2978 pushNewFreeBusy(new FreeBusy(freeBusy.getOption()));
2979 if (end.getTime() < curFreeBusyItem.getEnd().getTime()) {
2980 var _f1 = new FreeBusy(curFreeBusyItem.getOption());
2981 _f1.setOption('start', end);
2982 pushNewFreeBusy(_f1);
2983 }
2984 endIndex = index;
2985 return false;
2986 }
2987 });
2988 //now compute arguments
2989 var tmpFB = this.freeBusys;
2990 this.freeBusys = [];
2991
2992 if (startIndex) {
2993 this.freeBusys = this.freeBusys.concat(tmpFB.slice(0, startIndex));
2994 }
2995 this.freeBusys = this.freeBusys.concat(newFreeBusys);
2996 if (endIndex < tmpFB.length) {
2997 this.freeBusys = this.freeBusys.concat(tmpFB.slice(endIndex + 1));
2998 }
2999/* if(start.getDate() == 1){
3000 console.info('insert from '+freeBusy.getStart() +' to '+freeBusy.getEnd());
3001 console.log('index from '+ startIndex + ' to ' + endIndex);
3002 var str = [];
3003 $.each(tmpFB, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' : 'busy'))});
3004 console.log(str.join('\n'));
3005
3006 console.log('insert');
3007 var str = [];
3008 $.each(newFreeBusys, function(i){str.push(this.getStart().getHours() + ' > ' + this.getEnd().getHours())});
3009 console.log(str.join(', '));
3010
3011 console.log('results');
3012 var str = [];
3013 $.each(this.freeBusys, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' :'busy'))});
3014 console.log(str.join('\n'));
3015 }*/
3016 return this;
3017 }
3018 });
3019
3020})(jQuery);