/* * jQuery.weekCalendar v2.0-dev * * for support join us at the google group: * - http://groups.google.com/group/jquery-week-calendar * have a look to the wiki for documentation: * - http://wiki.github.com/themouette/jquery-week-calendar/ * something went bad ? report an issue: * - http://github.com/themouette/jquery-week-calendar/issues * get the last version on github: * - http://github.com/themouette/jquery-week-calendar * * Copyright (c) 2009 Rob Monie * Copyright (c) 2010 Julien MUETTON * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * If you're after a monthly calendar plugin, check out this one : * http://arshaw.com/fullcalendar/ */ (function($) { // check the jquery version var _v = $.fn.jquery.split('.'), _jQuery14OrLower = (10 * _v[0] + _v[1]) < 15; $.widget('ui.weekCalendar', (function() { var _currentAjaxCall, _hourLineTimeout; return { options: { date: new Date(), timeFormat: null, dateFormat: 'M d, Y', alwaysDisplayTimeMinutes: true, use24Hour: false, daysToShow: 7, minBodyHeight: 100, firstDayOfWeek: function(calendar) { if ($(calendar).weekCalendar('option', 'daysToShow') != 5) { return 0; } else { //workweek return 1; } }, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday useShortDayNames: false, timeSeparator: ' to ', startParam: 'start', endParam: 'end', businessHours: {start: 8, end: 18, limitDisplay: false}, newEventText: 'New Event', timeslotHeight: 20, defaultEventLength: 2, timeslotsPerHour: 4, minDate: null, maxDate: null, showHeader: true, buttons: true, buttonText: { today: 'today', lastWeek: 'previous', nextWeek: 'next' }, switchDisplay: {}, scrollToHourMillis: 500, allowEventDelete: false, allowCalEventOverlap: false, overlapEventsSeparate: false, totalEventsWidthPercentInOneColumn: 100, readonly: false, allowEventCreation: true, hourLine: false, deletable: function(calEvent, element) { return true; }, draggable: function(calEvent, element) { return true; }, resizable: function(calEvent, element) { return true; }, eventClick: function(calEvent, element, dayFreeBusyManager, calendar, clickEvent) { }, eventRender: function(calEvent, element) { return element; }, eventAfterRender: function(calEvent, element) { return element; }, eventRefresh: function(calEvent, element) { return element; }, eventDrag: function(calEvent, element) { }, eventDrop: function(calEvent, element) { }, eventResize: function(calEvent, element) { }, eventNew: function(calEvent, element, dayFreeBusyManager, calendar, mouseupEvent) { }, eventMouseover: function(calEvent, $event) { }, eventMouseout: function(calEvent, $event) { }, eventDelete: function(calEvent, element, dayFreeBusyManager, calendar, clickEvent) { calendar.weekCalendar('removeEvent',calEvent.id); }, calendarBeforeLoad: function(calendar) { }, calendarAfterLoad: function(calendar) { }, noEvents: function() { }, eventHeader: function(calEvent, calendar) { var options = calendar.weekCalendar('option'); var one_hour = 3600000; var displayTitleWithTime = calEvent.end.getTime() - calEvent.start.getTime() <= (one_hour / options.timeslotsPerHour); if (displayTitleWithTime) { return calendar.weekCalendar( 'formatTime', calEvent.start) + ': ' + calEvent.title; } else { return calendar.weekCalendar( 'formatTime', calEvent.start) + options.timeSeparator + calendar.weekCalendar( 'formatTime', calEvent.end); } }, eventBody: function(calEvent, calendar) { return calEvent.title; }, shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], /* multi-users options */ /** * the available users for calendar. * if you want to display users separately, enable the * showAsSeparateUsers option. * if you provide a list of user and do not enable showAsSeparateUsers * option, then only the events that belongs to one or several of * given users will be displayed * @type {array} */ users: [], /** * should the calendar be displayed with separate column for each * users. * note that this option does nothing if you do not provide at least * one user. * @type {boolean} */ showAsSeparateUsers: true, /** * callback used to read user id from a user object. * @param {Object} user the user to retrieve the id from. * @param {number} index the user index from user list. * @param {jQuery} calendar the calendar object. * @return {int|String} the user id. */ getUserId: function(user, index, calendar) { return index; }, /** * callback used to read user name from a user object. * @param {Object} user the user to retrieve the name from. * @param {number} index the user index from user list. * @param {jQuery} calendar the calendar object. * @return {String} the user name. */ getUserName: function(user, index, calendar) { return user; }, /** * reads the id(s) of user(s) for who the event should be displayed. * @param {Object} calEvent the calEvent to read informations from. * @param {jQuery} calendar the calendar object. * @return {number|String|Array} the user id(s) to appened events for. */ getEventUserId: function(calEvent, calendar) { return calEvent.userId; }, /** * sets user id(s) to the calEvent * @param {Object} calEvent the calEvent to set informations to. * @param {jQuery} calendar the calendar object. * @return {Object} the calEvent with modified user id. */ setEventUserId: function(userId, calEvent, calendar) { calEvent.userId = userId; return calEvent; }, /* freeBusy options */ /** * should the calendar display freebusys ? * @type {boolean} */ displayFreeBusys: false, /** * read the id(s) for who the freebusy is available * @param {Object} calEvent the calEvent to read informations from. * @param {jQuery} calendar the calendar object. * @return {number|String|Array} the user id(s) to appened events for. */ getFreeBusyUserId: function(calFreeBusy, calendar) { return calFreeBusy.userId; }, /** * the default freeBusy object, used to manage default state * @type {Object} */ defaultFreeBusy: {free: false}, /** * function used to display the freeBusy element * @type {Function} * @param {Object} freeBusy the freeBusy timeslot to render. * @param {jQuery} $freeBusy the freeBusy HTML element. * @param {jQuery} calendar the calendar element. */ freeBusyRender: function(freeBusy, $freeBusy, calendar) { if (!freeBusy.free) { $freeBusy.addClass('free-busy-busy'); } else { $freeBusy.addClass('free-busy-free'); } return $freeBusy; }, /* other options */ /** * true means start on first day of week, false means starts on * startDate. * @param {jQuery} calendar the calendar object. * @type {Function|bool} */ startOnFirstDayOfWeek: function(calendar) { return $(calendar).weekCalendar('option', 'daysToShow') >= 5; }, /** * should the columns be rendered alternatively using odd/even * class * @type {boolean} */ displayOddEven: false, textSize: 13, /** * the title attribute for the calendar. possible placeholders are: * * @type {Function|string} * @param {number} option daysToShow. * @return {String} the title attribute for the calendar. */ title: '%start% - %end%', /** * default options to pass to callback * you can pass a function returning an object or a litteral object * @type {object|function(#calendar)} */ jsonOptions: {}, headerSeparator: '
', /** * returns formatted header for day display * @type {function(date,calendar)} */ getHeaderDate: null, preventDragOnEventCreation: false, /** * the event on which to bind calendar resize * @type {string} */ resizeEvent: 'resize.weekcalendar' }, /*********************** * Initialise calendar * ***********************/ _create: function() { var self = this; self._computeOptions(); self._setupEventDelegation(); self._renderCalendar(); self._loadCalEvents(); self._resizeCalendar(); self._scrollToHour(self.options.date.getHours(), true); if (this.options.resizeEvent) { $(window).unbind(this.options.resizeEvent); $(window).bind(this.options.resizeEvent, function() { self._resizeCalendar(); }); } }, /******************** * public functions * ********************/ /* * Refresh the events for the currently displayed week. */ refresh: function() { //reload with existing week this._loadCalEvents(this.element.data('startDate')); }, /* * Clear all events currently loaded into the calendar */ clear: function() { this._clearCalendar(); }, /* * Go to this week */ today: function() { this._clearCalendar(); this._loadCalEvents(new Date()); }, /* * Go to the previous week relative to the currently displayed week */ prevWeek: function() { //minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies var newDate = new Date(this.element.data('startDate').getTime() - (MILLIS_IN_WEEK / 6)); this._clearCalendar(); this._loadCalEvents(newDate); }, /* * Go to the next week relative to the currently displayed week */ nextWeek: function() { //add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies var newDate = new Date(this.element.data('startDate').getTime() + MILLIS_IN_WEEK + MILLIS_IN_DAY); this._clearCalendar(); this._loadCalEvents(newDate); }, /* * Reload the calendar to whatever week the date passed in falls on. */ gotoWeek: function(date) { this._clearCalendar(); this._loadCalEvents(date); }, /* * Reload the calendar to whatever week the date passed in falls on. */ gotoDate: function(date) { this._clearCalendar(); this._loadCalEvents(date); }, /** * change the number of days to show */ setDaysToShow: function(daysToShow) { var self = this; var hour = self._getCurrentScrollHour(); self.options.daysToShow = daysToShow; $(self.element).html(''); self._renderCalendar(); self._loadCalEvents(); self._resizeCalendar(); self._scrollToHour(hour, false); if (this.options.resizeEvent) { $(window).unbind(this.options.resizeEvent); $(window).bind(this.options.resizeEvent, function() { self._resizeCalendar(); }); } }, /* * Remove an event based on it's id */ removeEvent: function(eventId) { var self = this; self.element.find('.wc-cal-event').each(function() { if ($(this).data('calEvent').id === eventId) { $(this).remove(); return false; } }); //this could be more efficient rather than running on all days regardless... self.element.find('.wc-day-column-inner').each(function() { self._adjustOverlappingEvents($(this)); }); }, /* * Removes any events that have been added but not yet saved (have no id). * This is useful to call after adding a freshly saved new event. */ removeUnsavedEvents: function() { var self = this; self.element.find('.wc-new-cal-event').each(function() { $(this).remove(); }); //this could be more efficient rather than running on all days regardless... self.element.find('.wc-day-column-inner').each(function() { self._adjustOverlappingEvents($(this)); }); }, /* * update an event in the calendar. If the event exists it refreshes * it's rendering. If it's a new event that does not exist in the calendar * it will be added. */ updateEvent: function(calEvent) { this._updateEventInCalendar(calEvent); }, /* * Returns an array of timeslot start and end times based on * the configured grid of the calendar. Returns in both date and * formatted time based on the 'timeFormat' config option. */ getTimeslotTimes: function(date) { var options = this.options; var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0; var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed); var times = [], startMillis = startDate.getTime(); for (var i = 0; i < options.timeslotsPerDay; i++) { var endMillis = startMillis + options.millisPerTimeslot; times[i] = { start: new Date(startMillis), startFormatted: this.formatTime(new Date(startMillis), options.timeFormat), end: new Date(endMillis), endFormatted: this.formatTime(new Date(endMillis), options.timeFormat) }; startMillis = endMillis; } return times; }, formatDate: function(date, format) { if (format) { return this._formatDate(date, format); } else { return this._formatDate(date, this.options.dateFormat); } }, formatTime: function(date, format) { if (format) { return this._formatDate(date, format); } else if (this.options.timeFormat) { return this._formatDate(date, this.options.timeFormat); } else if (this.options.use24Hour) { return this._formatDate(date, 'H:i'); } else { return this._formatDate(date, 'h:i a'); } }, serializeEvents: function() { var self = this; var calEvents = []; self.element.find('.wc-cal-event').each(function() { calEvents.push($(this).data('calEvent')); }); return calEvents; }, next: function() { if (this._startOnFirstDayOfWeek()) { return this.nextWeek(); } var newDate = new Date(this.element.data('startDate').getTime()); newDate.setDate(newDate.getDate() + this.options.daysToShow); this._clearCalendar(); this._loadCalEvents(newDate); }, prev: function() { if (this._startOnFirstDayOfWeek()) { return this.prevWeek(); } var newDate = new Date(this.element.data('startDate').getTime()); newDate.setDate(newDate.getDate() - this.options.daysToShow); this._clearCalendar(); this._loadCalEvents(newDate); }, getCurrentFirstDay: function() { return this._dateFirstDayOfWeek(this.options.date || new Date()); }, getCurrentLastDay: function() { return this._addDays(this.getCurrentFirstDay(), this.options.daysToShow - 1); }, /********************* * private functions * *********************/ _setOption: function(key, value) { var self = this; if (self.options[key] != value) { // event callback change, no need to re-render the events if (key == 'beforeEventNew') { self.options[key] = value; return; } // this could be made more efficient at some stage by caching the // events array locally in a store but this should be done in conjunction // with a proper binding model. var currentEvents = self.element.find('.wc-cal-event').map(function() { return $(this).data('calEvent'); }); var newOptions = {}; newOptions[key] = value; self._renderEvents({events: currentEvents, options: newOptions}, self.element.find('.wc-day-column-inner')); } }, // compute dynamic options based on other config values _computeOptions: function() { var options = this.options; if (options.businessHours.limitDisplay) { options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start); options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 3600000; // 60 * 60 * 1000 options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay; } else { options.timeslotsPerDay = options.timeslotsPerHour * 24; options.millisToDisplay = MILLIS_IN_DAY; options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay; } }, /* * Resize the calendar scrollable height based on the provided function in options. */ _resizeCalendar: function() { var options = this.options; if (options && $.isFunction(options.height)) { var calendarHeight = options.height(this.element); var headerHeight = this.element.find('.wc-header').outerHeight(); var navHeight = this.element.find('.wc-toolbar').outerHeight(); var scrollContainerHeight = Math.max(calendarHeight - navHeight - headerHeight, options.minBodyHeight); var timeslotHeight = this.element.find('.wc-time-slots').outerHeight(); this.element.find('.wc-scrollable-grid').height(scrollContainerHeight); if (timeslotHeight <= scrollContainerHeight) { this.element.find('.wc-scrollbar-shim').width(0); } else { this.element.find('.wc-scrollbar-shim').width(this._findScrollBarWidth()); } this._trigger('resize', this.element); } }, _findScrollBarWidth: function() { var parent = $('
').appendTo('body'); var child = parent.children(); var width = child.innerWidth() - child.height(99).innerWidth(); parent.remove(); return width || /* default to 16 that is the average */ 16; }, /* * configure calendar interaction events that are able to use event * delegation for greater efficiency */ _setupEventDelegation: function() { var self = this; var options = this.options; this.element.click(function(event) { var $target = $(event.target), freeBusyManager; // click is disabled if ($target.data('preventClick')) { return; } var $calEvent = $target.hasClass('wc-cal-event') ? $target : $target.parents('.wc-cal-event'); if (!$calEvent.length || !$calEvent.data('calEvent')) { return; } freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent')); if (options.allowEventDelete && $target.hasClass('wc-cal-event-delete')) { options.eventDelete($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event); } else { options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event); } }).mouseover(function(event) { var $target = $(event.target); var $calEvent = $target.hasClass('wc-cal-event') ? $target : $target.parents('.wc-cal-event'); if (!$calEvent.length || !$calEvent.data('calEvent')) { return; } if (self._isDraggingOrResizing($calEvent)) { return; } options.eventMouseover($calEvent.data('calEvent'), $calEvent, event); }).mouseout(function(event) { var $target = $(event.target); var $calEvent = $target.hasClass('wc-cal-event') ? $target : $target.parents('.wc-cal-event'); if (!$calEvent.length || !$calEvent.data('calEvent')) { return; } if (self._isDraggingOrResizing($calEvent)) { return; } options.eventMouseout($calEvent.data('calEvent'), $calEvent, event); }); }, /** * check if a ui draggable or resizable is currently being dragged or * resized. */ _isDraggingOrResizing: function($target) { return $target.hasClass('ui-draggable-dragging') || $target.hasClass('ui-resizable-resizing'); }, /* * Render the main calendar layout */ _renderCalendar: function() { var $calendarContainer, $weekDayColumns; var self = this; var options = this.options; $calendarContainer = $('
').appendTo(self.element); //render the different parts // nav links self._renderCalendarButtons($calendarContainer); // header self._renderCalendarHeader($calendarContainer); // body self._renderCalendarBody($calendarContainer); $weekDayColumns = $calendarContainer.find('.wc-day-column-inner'); $weekDayColumns.each(function(i, val) { if (!options.readonly) { self._addDroppableToWeekDay($(this)); if (options.allowEventCreation) { self._setupEventCreationForWeekDay($(this)); } } }); }, /** * render the nav buttons on top of the calendar */ _renderCalendarButtons: function($calendarContainer) { var self = this, options = this.options; if ( !options.showHeader ) return; if (options.buttons) { var calendarNavHtml = ''; calendarNavHtml += '
'; calendarNavHtml += '
'; calendarNavHtml += '
'; calendarNavHtml += ''; calendarNavHtml += ''; calendarNavHtml += ''; calendarNavHtml += '
'; calendarNavHtml += '

'; calendarNavHtml += '
'; $(calendarNavHtml).appendTo($calendarContainer); $calendarContainer.find('.wc-nav .wc-today') .button({ icons: {primary: 'ui-icon-home'}}) .click(function() { self.today(); return false; }); $calendarContainer.find('.wc-nav .wc-prev') .button({ text: false, icons: {primary: 'ui-icon-seek-prev'}}) .click(function() { self.element.weekCalendar('prev'); return false; }); $calendarContainer.find('.wc-nav .wc-next') .button({ text: false, icons: {primary: 'ui-icon-seek-next'}}) .click(function() { self.element.weekCalendar('next'); return false; }); // now add buttons to switch display if (this.options.switchDisplay && $.isPlainObject(this.options.switchDisplay)) { var $container = $calendarContainer.find('.wc-display'); $.each(this.options.switchDisplay, function(label, option) { var _id = 'wc-switch-display-' + option; var _input = $(''); var _label = $(''); _label.html(label); _input.val(option); if (parseInt(self.options.daysToShow, 10) === parseInt(option, 10)) { _input.attr('checked', 'checked'); } $container .append(_input) .append(_label); }); $container.find('input').change(function() { self.setDaysToShow(parseInt($(this).val(), 10)); }); } $calendarContainer.find('.wc-nav, .wc-display').buttonset(); var _height = $calendarContainer.find('.wc-nav').outerHeight(); $calendarContainer.find('.wc-title') .height(_height) .css('line-height', _height + 'px'); }else{ var calendarNavHtml = ''; calendarNavHtml += '
'; calendarNavHtml += '

'; calendarNavHtml += '
'; $(calendarNavHtml).appendTo($calendarContainer); } }, /** * render the calendar header, including date and user header */ _renderCalendarHeader: function($calendarContainer) { var self = this, options = this.options, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, rowspan = '', colspan = '', calendarHeaderHtml; if (showAsSeparatedUser) { rowspan = ' rowspan=\"2\"'; colspan = ' colspan=\"' + options.users.length + '\" '; } //first row calendarHeaderHtml = '
'; calendarHeaderHtml += ''; for (var i = 1; i <= options.daysToShow; i++) { calendarHeaderHtml += ''; } calendarHeaderHtml += ''; //users row if (showAsSeparatedUser) { calendarHeaderHtml += ''; var uLength = options.users.length, _headerClass = ''; for (var i = 1; i <= options.daysToShow; i++) { for (var j = 0; j < uLength; j++) { _headerClass = []; if (j == 0) { _headerClass.push('wc-day-column-first'); } if (j == uLength - 1) { _headerClass.push('wc-day-column-last'); } if (!_headerClass.length) { _headerClass = 'wc-day-column-middle'; } else { _headerClass = _headerClass.join(' '); } calendarHeaderHtml += ''; } } calendarHeaderHtml += ''; } //close the header calendarHeaderHtml += '
'; // calendarHeaderHtml+= "
"; calendarHeaderHtml += self._getUserName(j); // calendarHeaderHtml+= "
"; calendarHeaderHtml += '
'; $(calendarHeaderHtml).appendTo($calendarContainer); }, /** * render the calendar body. * Calendar body is composed of several distinct parts. * Each part is displayed in a separated row to ease rendering. * for further explanations, see each part rendering function. */ _renderCalendarBody: function($calendarContainer) { var self = this, options = this.options, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, $calendarBody, $calendarTableTbody; // create the structure $calendarBody = '
'; $calendarBody += ''; $calendarBody += ''; $calendarBody += ''; $calendarBody += '
'; $calendarBody += '
'; $calendarBody = $($calendarBody); $calendarTableTbody = $calendarBody.find('tbody'); self._renderCalendarBodyTimeSlots($calendarTableTbody); self._renderCalendarBodyOddEven($calendarTableTbody); self._renderCalendarBodyFreeBusy($calendarTableTbody); self._renderCalendarBodyEvents($calendarTableTbody); $calendarBody.appendTo($calendarContainer); //set the column height $calendarContainer.find('.wc-full-height-column').height(options.timeslotHeight * options.timeslotsPerDay); //set the timeslot height $calendarContainer.find('.wc-time-slot').height(options.timeslotHeight - 1); //account for border //init the time row header height /** TODO if total height for an hour is less than 11px, there is a display problem. Find a way to handle it */ $calendarContainer.find('.wc-time-header-cell').css({ height: (options.timeslotHeight * options.timeslotsPerHour) - 11, padding: 5 }); //add the user data to every impacted column if (showAsSeparatedUser) { for (var i = 0, uLength = options.users.length; i < uLength; i++) { $calendarContainer.find('.wc-user-' + self._getUserIdFromIndex(i)) .data('wcUser', options.users[i]) .data('wcUserIndex', i) .data('wcUserId', self._getUserIdFromIndex(i)); } } }, /** * render the timeslots separation */ _renderCalendarBodyTimeSlots: function($calendarTableTbody) { var options = this.options, renderRow, i, j, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, start = (options.businessHours.limitDisplay ? options.businessHours.start : 0), end = (options.businessHours.limitDisplay ? options.businessHours.end : 24), rowspan = 1; //calculate the rowspan if (options.displayOddEven) { rowspan += 1; } if (options.displayFreeBusys) { rowspan += 1; } if (rowspan > 1) { rowspan = ' rowspan=\"' + rowspan + '\"'; } else { rowspan = ''; } renderRow = ''; renderRow += ''; renderRow += ''; renderRow += '
'; renderRow += '
'; for (i = start; i < end; i++) { for (j = 0; j < options.timeslotsPerHour - 1; j++) { renderRow += '
'; } renderRow += '
'; } renderRow += '
'; renderRow += '
'; renderRow += ''; renderRow += ''; $(renderRow).appendTo($calendarTableTbody); }, /** * render the odd even columns */ _renderCalendarBodyOddEven: function($calendarTableTbody) { if (this.options.displayOddEven) { var options = this.options, renderRow = '', showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, oddEven, // let's take advantage of the jquery ui framework oddEvenClasses = {'odd': 'wc-column-odd', 'even': 'ui-state-hover wc-column-even'}; //now let's display oddEven placeholders for (var i = 1; i <= options.daysToShow; i++) { if (!showAsSeparatedUser) { oddEven = (oddEven == 'odd' ? 'even' : 'odd'); renderRow += ''; renderRow += '
'; renderRow += '
'; renderRow += '
'; renderRow += ''; } else { var uLength = options.users.length; for (var j = 0; j < uLength; j++) { oddEven = (oddEven == 'odd' ? 'even' : 'odd'); renderRow += ''; renderRow += '
'; renderRow += '
'; renderRow += '
'; renderRow += ''; } } } renderRow += ''; $(renderRow).appendTo($calendarTableTbody); } }, /** * render the freebusy placeholders */ _renderCalendarBodyFreeBusy: function($calendarTableTbody) { if (this.options.displayFreeBusys) { var self = this, options = this.options, renderRow = '', showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length; renderRow += ''; //now let's display freebusy placeholders for (var i = 1; i <= options.daysToShow; i++) { if (options.displayFreeBusys) { if (!showAsSeparatedUser) { renderRow += ''; renderRow += '
'; renderRow += '
'; renderRow += '
'; renderRow += ''; } else { var uLength = options.users.length; for (var j = 0; j < uLength; j++) { renderRow += ''; renderRow += '
'; renderRow += '
'; renderRow += '
'; renderRow += '
'; renderRow += ''; } } } } renderRow += ''; $(renderRow).appendTo($calendarTableTbody); } }, /** * render the calendar body for event placeholders */ _renderCalendarBodyEvents: function($calendarTableTbody) { var self = this, options = this.options, renderRow, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, start = (options.businessHours.limitDisplay ? options.businessHours.start : 0), end = (options.businessHours.limitDisplay ? options.businessHours.end : 24); renderRow = ''; renderRow += ''; for (var i = start; i < end; i++) { var bhClass = (options.businessHours.start <= i && options.businessHours.end > i) ? 'ui-state-active wc-business-hours' : 'ui-state-default'; renderRow += '
'; if (options.use24Hour) { renderRow += '
' + self._24HourForIndex(i) + '
'; } else { renderRow += '
' + self._hourForIndex(i) + '' + self._amOrPm(i) + '
'; } renderRow += '
'; } renderRow += ''; //now let's display events placeholders var _columnBaseClass = 'ui-state-default wc-day-column'; for (var i = 1; i <= options.daysToShow; i++) { if (!showAsSeparatedUser) { renderRow += ''; renderRow += '
'; renderRow += ''; } else { var uLength = options.users.length; var columnclass; for (var j = 0; j < uLength; j++) { columnclass = []; if (j == 0) { columnclass.push('wc-day-column-first'); } if (j == uLength - 1) { columnclass.push('wc-day-column-last'); } if (!columnclass.length) { columnclass = 'wc-day-column-middle'; } else { columnclass = columnclass.join(' '); } renderRow += ''; renderRow += '
'; renderRow += '
'; renderRow += ''; } } } renderRow += ''; $(renderRow).appendTo($calendarTableTbody); }, /* * setup mouse events for capturing new events */ _setupEventCreationForWeekDay: function($weekDay) { var self = this; var options = this.options; $weekDay.mousedown(function(event) { var $target = $(event.target); if ($target.hasClass('wc-day-column-inner')) { var $newEvent = $('
'); $newEvent.css({lineHeight: (options.timeslotHeight - 2) + 'px', fontSize: (options.timeslotHeight / 2) + 'px'}); $target.append($newEvent); var columnOffset = $target.offset().top; var clickY = event.pageY - columnOffset; var clickYRounded = (clickY - (clickY % options.timeslotHeight)) / options.timeslotHeight; var topPosition = clickYRounded * options.timeslotHeight; $newEvent.css({top: topPosition}); if (!options.preventDragOnEventCreation) { $target.bind('mousemove.newevent', function(event) { $newEvent.show(); $newEvent.addClass('ui-resizable-resizing'); var height = Math.round(event.pageY - columnOffset - topPosition); var remainder = height % options.timeslotHeight; //snap to closest timeslot if (remainder < 0) { var useHeight = height - remainder; $newEvent.css('height', useHeight < options.timeslotHeight ? options.timeslotHeight : useHeight); } else { $newEvent.css('height', height + (options.timeslotHeight - remainder)); } }).mouseup(function() { $target.unbind('mousemove.newevent'); $newEvent.addClass('ui-corner-all'); }); } } }).mouseup(function(event) { var $target = $(event.target); var $weekDay = $target.closest('.wc-day-column-inner'); var $newEvent = $weekDay.find('.wc-new-cal-event-creating'); if ($newEvent.length) { var createdFromSingleClick = !$newEvent.hasClass('ui-resizable-resizing'); //if even created from a single click only, default height if (createdFromSingleClick) { $newEvent.css({height: options.timeslotHeight * options.defaultEventLength}).show(); } var top = parseInt($newEvent.css('top')); var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $newEvent, top); $newEvent.remove(); var newCalEvent = {start: eventDuration.start, end: eventDuration.end, title: options.newEventText}; var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length; if (showAsSeparatedUser) { newCalEvent = self._setEventUserId(newCalEvent, $weekDay.data('wcUserId')); } else if (!options.showAsSeparateUsers && options.users && options.users.length == 1) { newCalEvent = self._setEventUserId(newCalEvent, self._getUserIdFromIndex(0)); } var freeBusyManager = self.getFreeBusyManagerForEvent(newCalEvent); var $renderedCalEvent = self._renderEvent(newCalEvent, $weekDay); if (!options.allowCalEventOverlap) { self._adjustForEventCollisions($weekDay, $renderedCalEvent, newCalEvent, newCalEvent); self._positionEvent($weekDay, $renderedCalEvent); } else { self._adjustOverlappingEvents($weekDay); } var proceed = self._trigger('beforeEventNew', event, { 'calEvent': newCalEvent, 'createdFromSingleClick': createdFromSingleClick, 'calendar': self.element }); if (proceed) { options.eventNew(newCalEvent, $renderedCalEvent, freeBusyManager, self.element, event); } else { $($renderedCalEvent).remove(); } } }); }, /* * load calendar events for the week based on the date provided */ _loadCalEvents: function(dateWithinWeek) { var date, weekStartDate, weekEndDate, $weekDayColumns; var self = this; var options = this.options; date = this._fixMinMaxDate(dateWithinWeek || options.date); // if date is not provided // or was not set // or is different than old one if ((!date || !date.getTime) || (!options.date || !options.date.getTime) || date.getTime() != options.date.getTime() ) { // trigger the changedate event this._trigger('changedate', this.element, date); } this.options.date = date; weekStartDate = self._dateFirstDayOfWeek(date); weekEndDate = self._dateLastMilliOfWeek(date); options.calendarBeforeLoad(self.element); self.element.data('startDate', weekStartDate); self.element.data('endDate', weekEndDate); $weekDayColumns = self.element.find('.wc-day-column-inner'); self._updateDayColumnHeader($weekDayColumns); //load events by chosen means if (typeof options.data == 'string') { if (options.loading) { options.loading(true); } if (_currentAjaxCall) { // first abort current request. if (!_jQuery14OrLower) { _currentAjaxCall.abort(); } else { // due to the fact that jquery 1.4 does not detect a request was // aborted, we need to replace the onreadystatechange and // execute the "complete" callback. _currentAjaxCall.onreadystatechange = null; _currentAjaxCall.abort(); _currentAjaxCall = null; if (options.loading) { options.loading(false); } } } var jsonOptions = self._getJsonOptions(); jsonOptions[options.startParam || 'start'] = Math.round(weekStartDate.getTime() / 1000); jsonOptions[options.endParam || 'end'] = Math.round(weekEndDate.getTime() / 1000); _currentAjaxCall = $.ajax({ url: options.data, data: jsonOptions, dataType: 'json', error: function(XMLHttpRequest, textStatus, errorThrown) { // only prevent error with jQuery 1.5 // see issue #34. thanks to dapplebeforedawn // (https://github.com/themouette/jquery-week-calendar/issues#issue/34) // for 1.5+, aborted request mean errorThrown == 'abort' // for prior version it means !errorThrown && !XMLHttpRequest.status // fixes #55 if (errorThrown != 'abort' && XMLHttpRequest.status != 0) { alert('unable to get data, error:' + textStatus); } }, success: function(data) { self._renderEvents(data, $weekDayColumns); }, complete: function() { _currentAjaxCall = null; if (options.loading) { options.loading(false); } } }); } else if ($.isFunction(options.data)) { options.data(weekStartDate, weekEndDate, function(data) { self._renderEvents(data, $weekDayColumns); }); } else if (options.data) { self._renderEvents(options.data, $weekDayColumns); } self._disableTextSelect($weekDayColumns); }, /** * Draws a thin line which indicates the current time. */ _drawCurrentHourLine: function() { var d = new Date(), options = this.options, businessHours = options.businessHours; // first, we remove the old hourline if it exists $('.wc-hourline', this.element).remove(); // the line does not need to be displayed if (businessHours.limitDisplay && d.getHours() > businessHours.end) { return; } // then we recreate it var paddingStart = businessHours.limitDisplay ? businessHours.start : 0; var nbHours = d.getHours() - paddingStart + d.getMinutes() / 60; var positionTop = nbHours * options.timeslotHeight * options.timeslotsPerHour; var lineWidth = $('.wc-scrollable-grid .wc-today', this.element).width() + 3; $('.wc-scrollable-grid .wc-today', this.element).append( $('
', { 'class': 'wc-hourline', style: 'top: ' + positionTop + 'px; width: ' + lineWidth + 'px' }) ); }, /* * update the display of each day column header based on the calendar week */ _updateDayColumnHeader: function($weekDayColumns) { var self = this; var options = this.options; var currentDay = self._cloneDate(self.element.data('startDate')); var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length; var todayClass = 'ui-state-active wc-today'; self.element.find('.wc-header td.wc-day-column-header').each(function(i, val) { $(this).html(self._getHeaderDate(currentDay)); if (self._isToday(currentDay)) { $(this).addClass(todayClass); } else { $(this).removeClass(todayClass); } currentDay = self._addDays(currentDay, 1); }); currentDay = self._cloneDate(self.element.data('startDate')); if (showAsSeparatedUser) { self.element.find('.wc-header td.wc-user-header').each(function(i, val) { if (self._isToday(currentDay)) { $(this).addClass(todayClass); } else { $(this).removeClass(todayClass); } currentDay = ((i + 1) % options.users.length) ? currentDay : self._addDays(currentDay, 1); }); } currentDay = self._cloneDate(self.element.data('startDate')); $weekDayColumns.each(function(i, val) { $(this).data('startDate', self._cloneDate(currentDay)); $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY))); if (self._isToday(currentDay)) { $(this).parent() .addClass(todayClass) .removeClass('ui-state-default'); } else { $(this).parent() .removeClass(todayClass) .addClass('ui-state-default'); } if (!showAsSeparatedUser || !((i + 1) % options.users.length)) { currentDay = self._addDays(currentDay, 1); } }); //now update the freeBusy placeholders if (options.displayFreeBusys) { currentDay = self._cloneDate(self.element.data('startDate')); self.element.find('.wc-grid-row-freebusy .wc-column-freebusy').each(function(i, val) { $(this).data('startDate', self._cloneDate(currentDay)); $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY))); if (!showAsSeparatedUser || !((i + 1) % options.users.length)) { currentDay = self._addDays(currentDay, 1); } }); } // now update the calendar title if (this.options.title) { var date = this.options.date, start = self._cloneDate(self.element.data('startDate')), end = self._dateLastDayOfWeek(new Date(this._cloneDate(self.element.data('endDate')).getTime() - (MILLIS_IN_DAY))), title = this._getCalendarTitle(), date_format = options.dateFormat; // replace the placeholders contained in the title title = title.replace('%start%', self._formatDate(start, date_format)); title = title.replace('%end%', self._formatDate(end, date_format)); title = title.replace('%date%', self._formatDate(date, date_format)); $('.wc-toolbar .wc-title', self.element).html(title); } //self._clearFreeBusys(); }, /** * Gets the calendar raw title. */ _getCalendarTitle: function() { if ($.isFunction(this.options.title)) { return this.options.title(this.options.daysToShow); } return this.options.title || ''; }, /** * Render the events into the calendar */ _renderEvents: function(data, $weekDayColumns) { var self = this; var options = this.options; var eventsToRender, nbRenderedEvents = 0; if (data.options) { var updateLayout = false; // update options $.each(data.options, function(key, value) { if (value !== options[key]) { options[key] = value; updateLayout = updateLayout || $.ui.weekCalendar.updateLayoutOptions[key]; } }); self._computeOptions(); if (updateLayout) { var hour = self._getCurrentScrollHour(); self.element.empty(); self._renderCalendar(); $weekDayColumns = self.element.find('.wc-time-slots .wc-day-column-inner'); self._updateDayColumnHeader($weekDayColumns); self._resizeCalendar(); self._scrollToHour(hour, false); } } this._clearCalendar(); if ($.isArray(data)) { eventsToRender = self._cleanEvents(data); } else if (data.events) { eventsToRender = self._cleanEvents(data.events); self._renderFreeBusys(data); } $.each(eventsToRender, function(i, calEvent) { // render a multi day event as various event : // thanks to http://github.com/fbeauchamp/jquery-week-calendar var initialStart = new Date(calEvent.start); var initialEnd = new Date(calEvent.end); var maxHour = self.options.businessHours.limitDisplay ? self.options.businessHours.end : 24; var minHour = self.options.businessHours.limitDisplay ? self.options.businessHours.start : 0; var start = new Date(initialStart); var startDate = self._formatDate(start, 'Ymd'); var endDate = self._formatDate(initialEnd, 'Ymd'); var $weekDay; var isMultiday = false; while (startDate < endDate) { calEvent.start = start; // end of this virual calEvent is set to the end of the day calEvent.end.setFullYear(start.getFullYear()); calEvent.end.setDate(start.getDate()); calEvent.end.setMonth(start.getMonth()); calEvent.end.setHours(maxHour, 0, 0); if (($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) { self._renderEvent(calEvent, $weekDay); nbRenderedEvents += 1; } // start is set to the begin of the new day start.setDate(start.getDate() + 1); start.setHours(minHour, 0, 0); startDate = self._formatDate(start, 'Ymd'); isMultiday = true; } if (start <= initialEnd) { calEvent.start = start; calEvent.end = initialEnd; if (((isMultiday && calEvent.start.getTime() != calEvent.end.getTime()) || !isMultiday) && ($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) { self._renderEvent(calEvent, $weekDay); nbRenderedEvents += 1; } } // put back the initial start date calEvent.start = initialStart; }); $weekDayColumns.each(function() { self._adjustOverlappingEvents($(this)); }); options.calendarAfterLoad(self.element); _hourLineTimeout && clearInterval(_hourLineTimeout); if (options.hourLine) { self._drawCurrentHourLine(); _hourLineTimeout = setInterval(function() { self._drawCurrentHourLine(); }, 60 * 1000); // redraw the line each minute } !nbRenderedEvents && options.noEvents(); }, /* * Render a specific event into the day provided. Assumes correct * day for calEvent date */ _renderEvent: function(calEvent, $weekDay) { var self = this; var options = this.options; if (calEvent.start.getTime() > calEvent.end.getTime()) { return; // can't render a negative height } var eventClass, eventHtml, $calEventList, $modifiedEvent; eventClass = calEvent.id ? 'wc-cal-event' : 'wc-cal-event wc-new-cal-event'; eventHtml = '
'; eventHtml += '
'; eventHtml += '
'; $weekDay.each(function() { var $calEvent = $(eventHtml); $modifiedEvent = options.eventRender(calEvent, $calEvent); $calEvent = $modifiedEvent ? $modifiedEvent.appendTo($(this)) : $calEvent.appendTo($(this)); $calEvent.css({lineHeight: (options.textSize + 2) + 'px', fontSize: options.textSize + 'px'}); self._refreshEventDetails(calEvent, $calEvent); self._positionEvent($(this), $calEvent); //add to event list if ($calEventList) { $calEventList = $calEventList.add($calEvent); } else { $calEventList = $calEvent; } }); $calEventList.show(); if (!options.readonly && options.resizable(calEvent, $calEventList)) { self._addResizableToCalEvent(calEvent, $calEventList, $weekDay); } if (!options.readonly && options.draggable(calEvent, $calEventList)) { self._addDraggableToCalEvent(calEvent, $calEventList); } options.eventAfterRender(calEvent, $calEventList); return $calEventList; }, addEvent: function() { return this._renderEvent.apply(this, arguments); }, _adjustOverlappingEvents: function($weekDay) { var self = this; if (self.options.allowCalEventOverlap) { var groupsList = self._groupOverlappingEventElements($weekDay); $.each(groupsList, function() { var curGroups = this; $.each(curGroups, function(groupIndex) { var curGroup = this; // do we want events to be displayed as overlapping if (self.options.overlapEventsSeparate) { var newWidth = self.options.totalEventsWidthPercentInOneColumn / curGroups.length; var newLeft = groupIndex * newWidth; } else { // TODO what happens when the group has more than 10 elements var newWidth = self.options.totalEventsWidthPercentInOneColumn - ((curGroups.length - 1) * 10); var newLeft = groupIndex * 10; } $.each(curGroup, function() { // bring mouseovered event to the front if (!self.options.overlapEventsSeparate) { $(this).bind('mouseover.z-index', function() { var $elem = $(this); $.each(curGroup, function() { $(this).css({'z-index': '1'}); }); $elem.css({'z-index': '3'}); }); } $(this).css({width: newWidth + '%', left: newLeft + '%', right: 0}); }); }); }); } }, /* * Find groups of overlapping events */ _groupOverlappingEventElements: function($weekDay) { var $events = $weekDay.find('.wc-cal-event:visible'); var sortedEvents = $events.sort(function(a, b) { return $(a).data('calEvent').start.getTime() - $(b).data('calEvent').start.getTime(); }); var lastEndTime = new Date(0, 0, 0); var groups = []; var curGroups = []; var $curEvent; $.each(sortedEvents, function() { $curEvent = $(this); //checks, if the current group list is not empty, if the overlapping is finished if (curGroups.length > 0) { if (lastEndTime.getTime() <= $curEvent.data('calEvent').start.getTime()) { //finishes the current group list by adding it to the resulting list of groups and cleans it groups.push(curGroups); curGroups = []; } } //finds the first group to fill with the event for (var groupIndex = 0; groupIndex < curGroups.length; groupIndex++) { if (curGroups[groupIndex].length > 0) { //checks if the event starts after the end of the last event of the group if (curGroups[groupIndex][curGroups[groupIndex].length - 1].data('calEvent').end.getTime() <= $curEvent.data('calEvent').start.getTime()) { curGroups[groupIndex].push($curEvent); if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) { lastEndTime = $curEvent.data('calEvent').end; } return; } } } //if not found, creates a new group curGroups.push([$curEvent]); if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) { lastEndTime = $curEvent.data('calEvent').end; } }); //adds the last groups in result if (curGroups.length > 0) { groups.push(curGroups); } return groups; }, /* * find the weekday in the current calendar that the calEvent falls within */ _findWeekDayForEvent: function(calEvent, $weekDayColumns) { var $weekDay, options = this.options, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, user_ids = this._getEventUserId(calEvent); if (!$.isArray(user_ids)) { user_ids = [user_ids]; } $weekDayColumns.each(function(index, curDay) { if ($(this).data('startDate').getTime() <= calEvent.start.getTime() && $(this).data('endDate').getTime() >= calEvent.end.getTime() && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), user_ids) !== -1) ) { if ($weekDay) { $weekDay = $weekDay.add($(curDay)); } else { $weekDay = $(curDay); } } }); return $weekDay; }, /* * update the events rendering in the calendar. Add if does not yet exist. */ _updateEventInCalendar: function(calEvent) { var self = this; self._cleanEvent(calEvent); if (calEvent.id) { self.element.find('.wc-cal-event').each(function() { if ($(this).data('calEvent').id === calEvent.id || $(this).hasClass('wc-new-cal-event')) { $(this).remove(); // return false; } }); } var $weekDays = self._findWeekDayForEvent(calEvent, self.element.find('.wc-grid-row-events .wc-day-column-inner')); if ($weekDays) { $weekDays.each(function(index, weekDay) { var $weekDay = $(weekDay); var $calEvent = self._renderEvent(calEvent, $weekDay); self._adjustForEventCollisions($weekDay, $calEvent, calEvent, calEvent); self._refreshEventDetails(calEvent, $calEvent); self._positionEvent($weekDay, $calEvent); self._adjustOverlappingEvents($weekDay); }); } }, /* * Position the event element within the weekday based on it's start / end dates. */ _positionEvent: function($weekDay, $calEvent) { var options = this.options; var calEvent = $calEvent.data('calEvent'); var pxPerMillis = $weekDay.height() / options.millisToDisplay; var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0; var startMillis = this._getDSTdayShift(calEvent.start).getTime() - this._getDSTdayShift(new Date(calEvent.start.getFullYear(), calEvent.start.getMonth(), calEvent.start.getDate(), firstHourDisplayed)).getTime(); var eventMillis = this._getDSTdayShift(calEvent.end).getTime() - this._getDSTdayShift(calEvent.start).getTime(); var pxTop = pxPerMillis * startMillis; var pxHeight = pxPerMillis * eventMillis; //var pxHeightFallback = pxPerMillis * (60 / options.timeslotsPerHour) * 60 * 1000; $calEvent.css({top: pxTop, height: pxHeight || (pxPerMillis * 3600000 / options.timeslotsPerHour)}); }, /* * Determine the actual start and end times of a calevent based on it's * relative position within the weekday column and the starting hour of the * displayed calendar. */ _getEventDurationFromPositionedEventElement: function($weekDay, $calEvent, top) { var options = this.options; var startOffsetMillis = options.businessHours.limitDisplay ? options.businessHours.start * 3600000 : 0; var start = new Date($weekDay.data('startDate').getTime() + startOffsetMillis + Math.round(top / options.timeslotHeight) * options.millisPerTimeslot); var end = new Date(start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot); return {start: this._getDSTdayShift(start, -1), end: this._getDSTdayShift(end, -1)}; }, /* * If the calendar does not allow event overlap, adjust the start or end date if necessary to * avoid overlapping of events. Typically, shortens the resized / dropped event to it's max possible * duration based on the overlap. If no satisfactory adjustment can be made, the event is reverted to * it's original location. */ _adjustForEventCollisions: function($weekDay, $calEvent, newCalEvent, oldCalEvent, maintainEventDuration) { var options = this.options; if (options.allowCalEventOverlap) { return; } var adjustedStart, adjustedEnd; var self = this; $weekDay.find('.wc-cal-event').not($calEvent).each(function() { var currentCalEvent = $(this).data('calEvent'); //has been dropped onto existing event overlapping the end time if (newCalEvent.start.getTime() < currentCalEvent.end.getTime() && newCalEvent.end.getTime() >= currentCalEvent.end.getTime()) { adjustedStart = currentCalEvent.end; } //has been dropped onto existing event overlapping the start time if (newCalEvent.end.getTime() > currentCalEvent.start.getTime() && newCalEvent.start.getTime() <= currentCalEvent.start.getTime()) { adjustedEnd = currentCalEvent.start; } //has been dropped inside existing event with same or larger duration if (oldCalEvent.resizable == false || (newCalEvent.end.getTime() <= currentCalEvent.end.getTime() && newCalEvent.start.getTime() >= currentCalEvent.start.getTime())) { adjustedStart = oldCalEvent.start; adjustedEnd = oldCalEvent.end; return false; } }); newCalEvent.start = adjustedStart || newCalEvent.start; if (adjustedStart && maintainEventDuration) { newCalEvent.end = new Date(adjustedStart.getTime() + (oldCalEvent.end.getTime() - oldCalEvent.start.getTime())); self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, oldCalEvent); } else { newCalEvent.end = adjustedEnd || newCalEvent.end; } //reset if new cal event has been forced to zero size if (newCalEvent.start.getTime() >= newCalEvent.end.getTime()) { newCalEvent.start = oldCalEvent.start; newCalEvent.end = oldCalEvent.end; } $calEvent.data('calEvent', newCalEvent); }, /** * Add draggable capabilities to an event */ _addDraggableToCalEvent: function(calEvent, $calEvent) { var options = this.options; $calEvent.draggable({ handle: '.wc-time', containment: 'div.wc-time-slots', snap: '.wc-day-column-inner', snapMode: 'inner', snapTolerance: options.timeslotHeight - 1, revert: 'invalid', opacity: 0.5, grid: [$calEvent.outerWidth() + 1, options.timeslotHeight], start: function(event, ui) { var $calEvent = ui.draggable || ui.helper; options.eventDrag(calEvent, $calEvent); } }); }, /* * Add droppable capabilites to weekdays to allow dropping of calEvents only */ _addDroppableToWeekDay: function($weekDay) { var self = this; var options = this.options; $weekDay.droppable({ accept: '.wc-cal-event', drop: function(event, ui) { var $calEvent = ui.draggable; var top = Math.round(parseInt(ui.position.top)); var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top); var calEvent = $calEvent.data('calEvent'); var newCalEvent = $.extend(true, {}, calEvent, {start: eventDuration.start, end: eventDuration.end}); var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length; if (showAsSeparatedUser) { // we may have dragged the event on column with a new user. // nice way to handle that is: // - get the newly dragged on user // - check if user is part of the event // - if yes, nothing changes, if not, find the old owner to remove it and add new one var newUserId = $weekDay.data('wcUserId'); var userIdList = self._getEventUserId(calEvent); var oldUserId = $(ui.draggable.parents('.wc-day-column-inner').get(0)).data('wcUserId'); if (!$.isArray(userIdList)) { userIdList = [userIdList]; } if ($.inArray(newUserId, userIdList) == -1) { // remove old user var _index = $.inArray(oldUserId, userIdList); userIdList.splice(_index, 1); // add new user ? if ($.inArray(newUserId, userIdList) == -1) { userIdList.push(newUserId); } } newCalEvent = self._setEventUserId(newCalEvent, ((userIdList.length == 1) ? userIdList[0] : userIdList)); } self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true); var $weekDayColumns = self.element.find('.wc-day-column-inner'); //trigger drop callback options.eventDrop(newCalEvent, calEvent, $calEvent); var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns)); $calEvent.hide(); $calEvent.data('preventClick', true); var $weekDayOld = self._findWeekDayForEvent($calEvent.data('calEvent'), self.element.find('.wc-time-slots .wc-day-column-inner')); if ($weekDayOld.data('startDate') != $weekDay.data('startDate')) { self._adjustOverlappingEvents($weekDayOld); } self._adjustOverlappingEvents($weekDay); setTimeout(function() { $calEvent.remove(); }, 1000); } }); }, /* * Add resizable capabilities to a calEvent */ _addResizableToCalEvent: function(calEvent, $calEvent, $weekDay) { var self = this; var options = this.options; $calEvent.resizable({ grid: options.timeslotHeight, containment: $weekDay, handles: 's', minHeight: options.timeslotHeight, stop: function(event, ui) { var $calEvent = ui.element; var newEnd = new Date($calEvent.data('calEvent').start.getTime() + Math.max(1, Math.round(ui.size.height / options.timeslotHeight)) * options.millisPerTimeslot); if (self._needDSTdayShift($calEvent.data('calEvent').start, newEnd)) newEnd = self._getDSTdayShift(newEnd, -1); var newCalEvent = $.extend(true, {}, calEvent, {start: calEvent.start, end: newEnd}); self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent); //trigger resize callback options.eventResize(newCalEvent, calEvent, $calEvent); self._refreshEventDetails(newCalEvent, $calEvent); self._positionEvent($weekDay, $calEvent); self._adjustOverlappingEvents($weekDay); $calEvent.data('preventClick', true); setTimeout(function() { $calEvent.removeData('preventClick'); }, 500); } }); $('.ui-resizable-handle', $calEvent).text('='); }, /* * Refresh the displayed details of a calEvent in the calendar */ _refreshEventDetails: function(calEvent, $calEvent) { var suffix = ''; if (!this.options.readonly && this.options.allowEventDelete && this.options.deletable(calEvent,$calEvent)) { suffix = '
'; } $calEvent.find('.wc-time').html(this.options.eventHeader(calEvent, this.element) + suffix); $calEvent.find('.wc-title').html(this.options.eventBody(calEvent, this.element)); $calEvent.data('calEvent', calEvent); this.options.eventRefresh(calEvent, $calEvent); }, /* * Clear all cal events from the calendar */ _clearCalendar: function() { this.element.find('.wc-day-column-inner div').remove(); this._clearFreeBusys(); }, /* * Scroll the calendar to a specific hour */ _scrollToHour: function(hour, animate) { var self = this; var options = this.options; var $scrollable = this.element.find('.wc-scrollable-grid'); var slot = hour; if (self.options.businessHours.limitDisplay) { if (hour <= self.options.businessHours.start) { slot = 0; } else if (hour >= self.options.businessHours.end) { slot = self.options.businessHours.end - self.options.businessHours.start - 1; } else { slot = hour - self.options.businessHours.start; } } var $target = this.element.find('.wc-grid-timeslot-header .wc-hour-header:eq(' + slot + ')'); $scrollable.animate({scrollTop: 0}, 0, function() { var targetOffset = $target.offset().top; var scroll = targetOffset - $scrollable.offset().top - $target.outerHeight(); if (animate) { $scrollable.animate({scrollTop: scroll}, options.scrollToHourMillis); } else { $scrollable.animate({scrollTop: scroll}, 0); } }); }, /* * find the hour (12 hour day) for a given hour index */ _hourForIndex: function(index) { if (index === 0) { //midnight return 12; } else if (index < 13) { //am return index; } else { //pm return index - 12; } }, _24HourForIndex: function(index) { if (index === 0) { //midnight return '00:00'; } else if (index < 10) { return '0' + index + ':00'; } else { return index + ':00'; } }, _amOrPm: function(hourOfDay) { return hourOfDay < 12 ? 'AM' : 'PM'; }, _isToday: function(date) { var clonedDate = this._cloneDate(date); this._clearTime(clonedDate); var today = new Date(); this._clearTime(today); return today.getTime() === clonedDate.getTime(); }, /* * Clean events to ensure correct format */ _cleanEvents: function(events) { var self = this; $.each(events, function(i, event) { self._cleanEvent(event); }); return events; }, /* * Clean specific event */ _cleanEvent: function(event) { if (event.date) { event.start = event.date; } event.start = this._cleanDate(event.start); event.end = this._cleanDate(event.end); if (!event.end) { event.end = this._addDays(this._cloneDate(event.start), 1); } }, /* * Disable text selection of the elements in different browsers */ _disableTextSelect: function($elements) { $elements.each(function() { if ($.browser.mozilla) {//Firefox $(this).css('MozUserSelect', 'none'); } else if ($.browser.msie) {//IE $(this).bind('selectstart', function() { return false; }); } else {//Opera, etc. $(this).mousedown(function() { return false; }); } }); }, /* * returns the date on the first millisecond of the week */ _dateFirstDayOfWeek: function(date) { var self = this; var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); var adjustedDate = new Date(midnightCurrentDate); adjustedDate.setDate(adjustedDate.getDate() - self._getAdjustedDayIndex(midnightCurrentDate)); return adjustedDate; }, /* * returns the date on the first millisecond of the last day of the week */ _dateLastDayOfWeek: function(date) { var self = this; var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); var adjustedDate = new Date(midnightCurrentDate); var daysToAdd = (self.options.daysToShow - 1 - self._getAdjustedDayIndex(midnightCurrentDate)); adjustedDate.setDate(adjustedDate.getDate() + daysToAdd); return adjustedDate; }, /** * fix the date if it is not within given options * minDate and maxDate */ _fixMinMaxDate: function(date) { var minDate, maxDate; date = this._cleanDate(date); // not less than minDate if (this.options.minDate) { minDate = this._cleanDate(this.options.minDate); // midnight on minDate minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()); if (date.getTime() < minDate.getTime()) { this._trigger('reachedmindate', this.element, date); } date = this._cleanDate(Math.max(date.getTime(), minDate.getTime())); } // not more than maxDate if (this.options.maxDate) { maxDate = this._cleanDate(this.options.maxDate); // apply correction for max date if not startOnFirstDayOfWeek // to make sure no further date is displayed. // otherwise, the complement will still be shown if (!this._startOnFirstDayOfWeek()) { var day = maxDate.getDate() - this.options.daysToShow + 1; maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), day); } // microsecond before midnight on maxDate maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999); if (date.getTime() > maxDate.getTime()) { this._trigger('reachedmaxdate', this.element, date); } date = this._cleanDate(Math.min(date.getTime(), maxDate.getTime())); } return date; }, /* * gets the index of the current day adjusted based on options */ _getAdjustedDayIndex: function(date) { if (!this._startOnFirstDayOfWeek()) { return 0; } var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); var currentDayOfStandardWeek = midnightCurrentDate.getDay(); var days = [0, 1, 2, 3, 4, 5, 6]; this._rotate(days, this._firstDayOfWeek()); return days[currentDayOfStandardWeek]; }, _firstDayOfWeek: function() { if ($.isFunction(this.options.firstDayOfWeek)) { return this.options.firstDayOfWeek(this.element); } return this.options.firstDayOfWeek; }, /* * returns the date on the last millisecond of the week */ _dateLastMilliOfWeek: function(date) { var lastDayOfWeek = this._dateLastDayOfWeek(date); lastDayOfWeek = this._cloneDate(lastDayOfWeek); lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 1); return lastDayOfWeek; }, /* * Clear the time components of a date leaving the date * of the first milli of day */ _clearTime: function(d) { d.setHours(0); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); return d; }, /* * add specific number of days to date */ _addDays: function(d, n, keepTime) { d.setDate(d.getDate() + n); if (keepTime) { return d; } return this._clearTime(d); }, /* * Rotate an array by specified number of places. */ _rotate: function(a /*array*/, p /* integer, positive integer rotate to the right, negative to the left... */) { 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)) { for (i = l; i > p; x = a[--i], a[i] = a[i - p], a[i - p] = x) {} } return a; }, _cloneDate: function(d) { return new Date(d.getTime()); }, /** * Return a Date instance for different representations. * Valid representations are: * * timestamps * * Date objects * * textual representations (only these accepted by the Date * constructor) * * @return {Date} The clean date object. */ _cleanDate: function(d) { if (typeof d === 'string') { // if is numeric if (!isNaN(Number(d))) { return this._cleanDate(parseInt(d, 10)); } // this is a human readable date return Date.parse(d) || new Date(d); } if (typeof d == 'number') { return new Date(d); } return d; }, /* * date formatting is adapted from * http://jacwright.com/projects/javascript/date_format */ _formatDate: function(date, format) { var returnStr = ''; for (var i = 0; i < format.length; i++) { var curChar = format.charAt(i); if (i != 0 && format.charAt(i - 1) == '\\') { returnStr += curChar; } else if (this._replaceChars[curChar]) { returnStr += this._replaceChars[curChar](date, this); } else if (curChar != '\\') { returnStr += curChar; } } return returnStr; }, _replaceChars: { // Day d: function(date) { return (date.getDate() < 10 ? '0' : '') + date.getDate(); }, D: function(date, calendar) { return calendar.options.shortDays[date.getDay()]; }, j: function(date) { return date.getDate(); }, l: function(date, calendar) { return calendar.options.longDays[date.getDay()]; }, N: function(date) { var _d = date.getDay(); return _d ? _d : 7; }, 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'))); }, w: function(date) { return date.getDay(); }, z: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((date - d) / 86400000); }, // Fixed now // Week W: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((((date - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now // Month F: function(date, calendar) { return calendar.options.longMonths[date.getMonth()]; }, m: function(date) { return (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1); }, M: function(date, calendar) { return calendar.options.shortMonths[date.getMonth()]; }, n: function(date) { return date.getMonth() + 1; }, t: function(date) { var d = date; return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() }, // Fixed now, gets #days of date // Year L: function(date) { var year = date.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now o: function(date) { var d = new Date(date.valueOf()); d.setDate(d.getDate() - ((date.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now Y: function(date) { return date.getFullYear(); }, y: function(date) { return ('' + date.getFullYear()).substr(2); }, // Time a: function(date) { return date.getHours() < 12 ? 'am' : 'pm'; }, A: function(date) { return date.getHours() < 12 ? 'AM' : 'PM'; }, B: function(date) { return Math.floor((((date.getUTCHours() + 1) % 24) + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now g: function(date) { return date.getHours() % 12 || 12; }, G: function(date) { return date.getHours(); }, h: function(date) { return ((date.getHours() % 12 || 12) < 10 ? '0' : '') + (date.getHours() % 12 || 12); }, H: function(date) { return (date.getHours() < 10 ? '0' : '') + date.getHours(); }, i: function(date) { return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); }, s: function(date) { return (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); }, u: function(date) { var m = date.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; }, // Timezone e: function(date) { return 'Not Yet Supported'; }, I: function(date) { return 'Not Yet Supported'; }, O: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + '00'; }, P: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now T: function(date) { var m = date.getMonth(); date.setMonth(0); var result = date.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); date.setMonth(m); return result;}, Z: function(date) { return -date.getTimezoneOffset() * 60; }, // Full Date/Time c: function(date, calendar) { return calendar._formatDate(date, 'Y-m-d\\TH:i:sP'); }, // Fixed now r: function(date, calendar) { return calendar._formatDate(date, 'D, d M Y H:i:s O'); }, U: function(date) { return date.getTime() / 1000; } }, /* USER MANAGEMENT FUNCTIONS */ getUserForId: function(id) { return $.extend({}, this.options.users[this._getUserIndexFromId(id)]); }, /** * return the user name for header */ _getUserName: function(index) { var self = this; var options = this.options; var user = options.users[index]; if ($.isFunction(options.getUserName)) { return options.getUserName(user, index, self.element); } else { return user; } }, /** * return the user id for given index */ _getUserIdFromIndex: function(index) { var self = this; var options = this.options; if ($.isFunction(options.getUserId)) { return options.getUserId(options.users[index], index, self.element); } return index; }, /** * returns the associated user index for given ID */ _getUserIndexFromId: function(id) { var self = this; var options = this.options; for (var i = 0; i < options.users.length; i++) { if (self._getUserIdFromIndex(i) == id) { return i; } } return 0; }, /** * return the user ids for given calEvent. * default is calEvent.userId field. */ _getEventUserId: function(calEvent) { var self = this; var options = this.options; if (options.showAsSeparateUsers && options.users && options.users.length) { if ($.isFunction(options.getEventUserId)) { return options.getEventUserId(calEvent, self.element); } return calEvent.userId; } return []; }, /** * sets the event user id on given calEvent * default is calEvent.userId field. */ _setEventUserId: function(calEvent, userId) { var self = this; var options = this.options; if ($.isFunction(options.setEventUserId)) { return options.setEventUserId(userId, calEvent, self.element); } calEvent.userId = userId; return calEvent; }, /** * return the user ids for given freeBusy. * default is freeBusy.userId field. */ _getFreeBusyUserId: function(freeBusy) { var self = this; var options = this.options; if ($.isFunction(options.getFreeBusyUserId)) { return options.getFreeBusyUserId(freeBusy.getOption(), self.element); } return freeBusy.getOption('userId'); }, /* FREEBUSY MANAGEMENT */ /** * ckean the free busy managers and remove all the freeBusy */ _clearFreeBusys: function() { if (this.options.displayFreeBusys) { var self = this, options = this.options, $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'); $freeBusyPlaceholders.each(function() { $(this).data('wcFreeBusyManager', new FreeBusyManager({ start: self._cloneDate($(this).data('startDate')), end: self._cloneDate($(this).data('endDate')), defaultFreeBusy: options.defaultFreeBusy || {} })); }); self.element.find('.wc-grid-row-freebusy .wc-freebusy').remove(); } }, /** * retrieve placeholders for given freebusy */ _findWeekDaysForFreeBusy: function(freeBusy, $weekDays) { var $returnWeekDays, options = this.options, showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, self = this, userList = self._getFreeBusyUserId(freeBusy); if (!$.isArray(userList)) { userList = userList != 'undefined' ? [userList] : []; } if (!$weekDays) { $weekDays = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'); } $weekDays.each(function() { var manager = $(this).data('wcFreeBusyManager'), has_overlap = manager.isWithin(freeBusy.getStart()) || manager.isWithin(freeBusy.getEnd()) || freeBusy.isWithin(manager.getStart()) || freeBusy.isWithin(manager.getEnd()), userId = $(this).data('wcUserId'); if (has_overlap && (!showAsSeparatedUser || ($.inArray(userId, userList) != -1))) { $returnWeekDays = $returnWeekDays ? $returnWeekDays.add($(this)) : $(this); } }); return $returnWeekDays; }, /** * used to render all freeBusys */ _renderFreeBusys: function(freeBusys) { if (this.options.displayFreeBusys) { var self = this, $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'), freebusysToRender; //insert freebusys to dedicated placeholders freebusy managers if ($.isArray(freeBusys)) { freebusysToRender = self._cleanFreeBusys(freeBusys); } else if (freeBusys.freebusys) { freebusysToRender = self._cleanFreeBusys(freeBusys.freebusys); } else { freebusysToRender = []; } $.each(freebusysToRender, function(index, freebusy) { var $placeholders = self._findWeekDaysForFreeBusy(freebusy, $freeBusyPlaceholders); if ($placeholders) { $placeholders.each(function() { var manager = $(this).data('wcFreeBusyManager'); manager.insertFreeBusy(new FreeBusy(freebusy.getOption())); $(this).data('wcFreeBusyManager', manager); }); } }); //now display freebusys on place holders self._refreshFreeBusys($freeBusyPlaceholders); } }, /** * refresh freebusys for given placeholders */ _refreshFreeBusys: function($freeBusyPlaceholders) { if (this.options.displayFreeBusys && $freeBusyPlaceholders) { var self = this, options = this.options, start = (options.businessHours.limitDisplay ? options.businessHours.start : 0), end = (options.businessHours.limitDisplay ? options.businessHours.end : 24); $freeBusyPlaceholders.each(function() { var $placehoder = $(this); var s = self._cloneDate($placehoder.data('startDate')), e = self._cloneDate(s); s.setHours(start); e.setHours(end); $placehoder.find('.wc-freebusy').remove(); $.each($placehoder.data('wcFreeBusyManager').getFreeBusys(s, e), function() { self._renderFreeBusy(this, $placehoder); }); }); } }, /** * render a freebusy item on dedicated placeholders */ _renderFreeBusy: function(freeBusy, $freeBusyPlaceholder) { if (this.options.displayFreeBusys) { var self = this, options = this.options, freeBusyHtml = '
'; var $fb = $(freeBusyHtml); $fb.data('wcFreeBusy', new FreeBusy(freeBusy.getOption())); this._positionFreeBusy($freeBusyPlaceholder, $fb); $fb = options.freeBusyRender(freeBusy.getOption(), $fb, self.element); if ($fb) { $fb.appendTo($freeBusyPlaceholder); } } }, /* * Position the freebusy element within the weekday based on it's start / end dates. */ _positionFreeBusy: function($placeholder, $freeBusy) { var options = this.options; var freeBusy = $freeBusy.data('wcFreeBusy'); var pxPerMillis = $placeholder.height() / options.millisToDisplay; var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0; var startMillis = freeBusy.getStart().getTime() - new Date(freeBusy.getStart().getFullYear(), freeBusy.getStart().getMonth(), freeBusy.getStart().getDate(), firstHourDisplayed).getTime(); var eventMillis = freeBusy.getEnd().getTime() - freeBusy.getStart().getTime(); var pxTop = pxPerMillis * startMillis; var pxHeight = pxPerMillis * eventMillis; $freeBusy.css({top: pxTop, height: pxHeight}); }, /* * Clean freebusys to ensure correct format */ _cleanFreeBusys: function(freebusys) { var self = this, freeBusyToReturn = []; if (!$.isArray(freebusys)) { var freebusys = [freebusys]; } $.each(freebusys, function(i, freebusy) { freeBusyToReturn.push(new FreeBusy(self._cleanFreeBusy(freebusy))); }); return freeBusyToReturn; }, /* * Clean specific freebusy */ _cleanFreeBusy: function(freebusy) { if (freebusy.date) { freebusy.start = freebusy.date; } freebusy.start = this._cleanDate(freebusy.start); freebusy.end = this._cleanDate(freebusy.end); return freebusy; }, /** * retrives the first freebusy manager matching demand. */ getFreeBusyManagersFor: function(date, users) { var calEvent = { start: date, end: date }; this._setEventUserId(calEvent, users); return this.getFreeBusyManagerForEvent(calEvent); }, /** * retrives the first freebusy manager for given event. */ getFreeBusyManagerForEvent: function(newCalEvent) { var self = this, options = this.options, freeBusyManager; if (options.displayFreeBusys) { var $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'), freeBusy = new FreeBusy({start: newCalEvent.start, end: newCalEvent.end}), showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length, userId = showAsSeparatedUser ? self._getEventUserId(newCalEvent) : null; if (!$.isArray(userId)) { userId = [userId]; } $freeBusyPlaceHoders.each(function() { var manager = $(this).data('wcFreeBusyManager'), has_overlap = manager.isWithin(freeBusy.getEnd()) || manager.isWithin(freeBusy.getEnd()) || freeBusy.isWithin(manager.getStart()) || freeBusy.isWithin(manager.getEnd()); if (has_overlap && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), userId) != -1)) { freeBusyManager = $(this).data('wcFreeBusyManager'); return false; } }); } return freeBusyManager; }, /** * appends the freebusys to replace the old ones. * @param {array|object} freeBusys freebusy(s) to apply. */ updateFreeBusy: function(freeBusys) { var self = this, options = this.options; if (options.displayFreeBusys) { var $toRender, $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'), _freeBusys = self._cleanFreeBusys(freeBusys); $.each(_freeBusys, function(index, _freeBusy) { var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHoders); //if freebusy has a placeholder if ($weekdays && $weekdays.length) { $weekdays.each(function(index, day) { var manager = $(day).data('wcFreeBusyManager'); manager.insertFreeBusy(_freeBusy); $(day).data('wcFreeBusyManager', manager); }); $toRender = $toRender ? $toRender.add($weekdays) : $weekdays; } }); self._refreshFreeBusys($toRender); } }, /* NEW OPTIONS MANAGEMENT */ /** * checks wether or not the calendar should be displayed starting on first day of week */ _startOnFirstDayOfWeek: function() { return jQuery.isFunction(this.options.startOnFirstDayOfWeek) ? this.options.startOnFirstDayOfWeek(this.element) : this.options.startOnFirstDayOfWeek; }, /** * finds out the current scroll to apply it when changing the view */ _getCurrentScrollHour: function() { var self = this; var options = this.options; var $scrollable = this.element.find('.wc-scrollable-grid'); var scroll = $scrollable.scrollTop(); if (self.options.businessHours.limitDisplay) { scroll = scroll + options.businessHours.start * options.timeslotHeight * options.timeslotsPerHour; } return Math.round(scroll / (options.timeslotHeight * options.timeslotsPerHour)) + 1; }, _getJsonOptions: function() { if ($.isFunction(this.options.jsonOptions)) { return $.extend({}, this.options.jsonOptions(this.element)); } if ($.isPlainObject(this.options.jsonOptions)) { return $.extend({}, this.options.jsonOptions); } return {}; }, _getHeaderDate: function(date) { var options = this.options; if (options.getHeaderDate && $.isFunction(options.getHeaderDate)) { return options.getHeaderDate(date, this.element); } var dayName = options.useShortDayNames ? options.shortDays[date.getDay()] : options.longDays[date.getDay()]; return dayName + (options.headerSeparator) + this._formatDate(date, options.dateFormat); }, /** * returns corrected date related to DST problem */ _getDSTdayShift: function(date, shift) { var start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0); var offset1 = start.getTimezoneOffset(); var offset2 = date.getTimezoneOffset(); if (offset1 == offset2) return date; shift = shift ? shift : 1; return new Date(date.getTime() - shift * (offset1 > offset2 ? -1 : 1) * (Math.max(offset1, offset2) - Math.min(offset1, offset2)) * 60000); }, _needDSTdayShift: function(date1, date2) { return date1.getTimezoneOffset() != date2.getTimezoneOffset(); } }; // end of widget function return })() //end of widget function closure execution ); // end of $.widget("ui.weekCalendar"... $.extend($.ui.weekCalendar, { version: '2.0-dev', updateLayoutOptions: { startOnFirstDayOfWeek: true, firstDayOfWeek: true, daysToShow: true, displayOddEven: true, timeFormat: true, dateFormat: true, use24Hour: true, useShortDayNames: true, businessHours: true, timeslotHeight: true, timeslotsPerHour: true, buttonText: true, height: true, shortMonths: true, longMonths: true, shortDays: true, longDays: true, textSize: true, users: true, showAsSeparateUsers: true, displayFreeBusys: true } }); var MILLIS_IN_DAY = 86400000; var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7; /* FREE BUSY MANAGERS */ var FreeBusyProto = { getStart: function() {return this.getOption('start')}, getEnd: function() {return this.getOption('end')}, getOption: function() { if (!arguments.length) { return this.options } if (typeof(this.options[arguments[0]]) !== 'undefined') { return this.options[arguments[0]]; } else if (typeof(arguments[1]) !== 'undefined') { return arguments[1]; } return null; }, setOption: function(key, value) { if (arguments.length == 1) { $.extend(this.options, arguments[0]); return this; } this.options[key] = value; return this; }, 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)}, isValid: function() {return this.getStart().getTime() < this.getEnd().getTime()} }; /** * @constructor * single user freebusy manager. */ var FreeBusy = function(options) { this.options = $.extend({}, options || {}); }; $.extend(FreeBusy.prototype, FreeBusyProto); var FreeBusyManager = function(options) { this.options = $.extend({ defaultFreeBusy: {} }, options || {}); this.freeBusys = []; this.freeBusys.push(new FreeBusy($.extend({ start: this.getStart(), end: this.getEnd() }, this.options.defaultFreeBusy))); }; $.extend(FreeBusyManager.prototype, FreeBusyProto, { /** * return matching freeBusys. * if you do not pass any argument, returns all freebusys. * if you only pass a start date, only matchinf freebusy will be returned. * if you pass 2 arguments, then all freebusys available within the time period will be returned * @param {Date} start [optionnal] if you do not pass end date, will return the freeBusy within which this date falls. * @param {Date} end [optionnal] the date where to stop the search. * @return {Array} an array of FreeBusy matching arguments. */ getFreeBusys: function() { switch (arguments.length) { case 0: return this.freeBusys; case 1: var freeBusy = []; var start = arguments[0]; if (!this.isWithin(start)) { return freeBusy; } $.each(this.freeBusys, function() { if (this.isWithin(start)) { freeBusy.push(this); } if (Math.floor(this.getEnd().getTime() / 1000) > Math.floor(start.getTime() / 1000)) { return false; } }); return freeBusy; default: //we assume only 2 first args are revealants var freeBusy = []; var start = arguments[0], end = arguments[1]; var tmpFreeBusy = new FreeBusy({start: start, end: end}); if (end.getTime() < start.getTime() || this.getStart().getTime() > end.getTime() || this.getEnd().getTime() < start.getTime()) { return freeBusy; } $.each(this.freeBusys, function() { if (this.getStart().getTime() >= end.getTime()) { return false; } if (tmpFreeBusy.isWithin(this.getStart()) && tmpFreeBusy.isWithin(this.getEnd())) { freeBusy.push(this); } else if (this.isWithin(tmpFreeBusy.getStart()) && this.isWithin(tmpFreeBusy.getEnd())) { var _f = new FreeBusy(this.getOption()); _f.setOption('end', tmpFreeBusy.getEnd()); _f.setOption('start', tmpFreeBusy.getStart()); freeBusy.push(_f); } else if (this.isWithin(tmpFreeBusy.getStart()) && this.getStart().getTime() < start.getTime()) { var _f = new FreeBusy(this.getOption()); _f.setOption('start', tmpFreeBusy.getStart()); freeBusy.push(_f); } else if (this.isWithin(tmpFreeBusy.getEnd()) && this.getEnd().getTime() > end.getTime()) { var _f = new FreeBusy(this.getOption()); _f.setOption('end', tmpFreeBusy.getEnd()); freeBusy.push(_f); } }); return freeBusy; } }, insertFreeBusy: function(freeBusy) { var freeBusy = new FreeBusy(freeBusy.getOption()); //first, if inserted freebusy is bigger than manager if (freeBusy.getStart().getTime() < this.getStart().getTime()) { freeBusy.setOption('start', this.getStart()); } if (freeBusy.getEnd().getTime() > this.getEnd().getTime()) { freeBusy.setOption('end', this.getEnd()); } var start = freeBusy.getStart(), end = freeBusy.getEnd(), startIndex = 0, endIndex = this.freeBusys.length - 1, newFreeBusys = []; var pushNewFreeBusy = function(_f) {if (_f.isValid()) newFreeBusys.push(_f);}; $.each(this.freeBusys, function(index) { //within the loop, we have following vars: // curFreeBusyItem: the current iteration freeBusy, part of manager freeBusys list // start: the insterted freeBusy start // end: the inserted freebusy end var curFreeBusyItem = this; if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.isWithin(end)) { /* we are in case where inserted freebusy fits in curFreeBusyItem: curFreeBusyItem: *-----------------------------* freeBusy: *-------------* obviously, start and end indexes are this item. */ startIndex = index; endIndex = index; if (start.getTime() == curFreeBusyItem.getStart().getTime() && end.getTime() == curFreeBusyItem.getEnd().getTime()) { /* in this case, inserted freebusy is exactly curFreeBusyItem: curFreeBusyItem: *-----------------------------* freeBusy: *-----------------------------* just replace curFreeBusyItem with freeBusy. */ var _f1 = new FreeBusy(freeBusy.getOption()); pushNewFreeBusy(_f1); } else if (start.getTime() == curFreeBusyItem.getStart().getTime()) { /* in this case inserted freebusy starts with curFreeBusyItem: curFreeBusyItem: *-----------------------------* freeBusy: *--------------* just replace curFreeBusyItem with freeBusy AND the rest. */ var _f1 = new FreeBusy(freeBusy.getOption()); var _f2 = new FreeBusy(curFreeBusyItem.getOption()); _f2.setOption('start', end); pushNewFreeBusy(_f1); pushNewFreeBusy(_f2); } else if (end.getTime() == curFreeBusyItem.getEnd().getTime()) { /* in this case inserted freebusy ends with curFreeBusyItem: curFreeBusyItem: *-----------------------------* freeBusy: *--------------* just replace curFreeBusyItem with before part AND freeBusy. */ var _f1 = new FreeBusy(curFreeBusyItem.getOption()); _f1.setOption('end', start); var _f2 = new FreeBusy(freeBusy.getOption()); pushNewFreeBusy(_f1); pushNewFreeBusy(_f2); } else { /* in this case inserted freebusy is within curFreeBusyItem: curFreeBusyItem: *-----------------------------* freeBusy: *--------------* just replace curFreeBusyItem with before part AND freeBusy AND the rest. */ var _f1 = new FreeBusy(curFreeBusyItem.getOption()); var _f2 = new FreeBusy(freeBusy.getOption()); var _f3 = new FreeBusy(curFreeBusyItem.getOption()); _f1.setOption('end', start); _f3.setOption('start', end); pushNewFreeBusy(_f1); pushNewFreeBusy(_f2); pushNewFreeBusy(_f3); } /* as work is done, no need to go further. return false */ return false; } else if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.getEnd().getTime() != start.getTime()) { /* in this case, inserted freebusy starts within curFreeBusyItem: curFreeBusyItem: *----------* freeBusy: *-------------------* set start index AND insert before part, we'll insert freebusy later */ if (curFreeBusyItem.getStart().getTime() != start.getTime()) { var _f1 = new FreeBusy(curFreeBusyItem.getOption()); _f1.setOption('end', start); pushNewFreeBusy(_f1); } startIndex = index; } else if (curFreeBusyItem.isWithin(end) && curFreeBusyItem.getStart().getTime() != end.getTime()) { /* in this case, inserted freebusy starts within curFreeBusyItem: curFreeBusyItem: *----------* freeBusy: *-------------------* set end index AND insert freebusy AND insert after part if needed */ pushNewFreeBusy(new FreeBusy(freeBusy.getOption())); if (end.getTime() < curFreeBusyItem.getEnd().getTime()) { var _f1 = new FreeBusy(curFreeBusyItem.getOption()); _f1.setOption('start', end); pushNewFreeBusy(_f1); } endIndex = index; return false; } }); //now compute arguments var tmpFB = this.freeBusys; this.freeBusys = []; if (startIndex) { this.freeBusys = this.freeBusys.concat(tmpFB.slice(0, startIndex)); } this.freeBusys = this.freeBusys.concat(newFreeBusys); if (endIndex < tmpFB.length) { this.freeBusys = this.freeBusys.concat(tmpFB.slice(endIndex + 1)); } /* if(start.getDate() == 1){ console.info('insert from '+freeBusy.getStart() +' to '+freeBusy.getEnd()); console.log('index from '+ startIndex + ' to ' + endIndex); var str = []; $.each(tmpFB, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' : 'busy'))}); console.log(str.join('\n')); console.log('insert'); var str = []; $.each(newFreeBusys, function(i){str.push(this.getStart().getHours() + ' > ' + this.getEnd().getHours())}); console.log(str.join(', ')); console.log('results'); var str = []; $.each(this.freeBusys, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' :'busy'))}); console.log(str.join('\n')); }*/ return this; } }); })(jQuery);