OpenXE/www/js/jquery.weekcalendar.js
2021-05-21 08:49:41 +02:00

2869 lines
114 KiB
JavaScript

/*
* 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;
return {
options: {
date: new Date(),
timeFormat: null,
dateFormat: 'M d, Y',
alwaysDisplayTimeMinutes: true,
use24Hour: true,
daysToShow: 5,
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: true,
timeSeparator: ' bis ',
startParam: 'start',
endParam: 'end',
businessHours: {start: 8, end: 18, limitDisplay: true},
newEventText: 'Neuen Termin',
timeslotHeight: 7,
defaultEventLength: 1,
timeslotsPerHour: 4,
minDate: null,
maxDate: null,
buttons: true,
buttonText: {
today: 'Heute',
lastWeek: 'previous',
nextWeek: 'next'
},
switchDisplay: {},
scrollToHourMillis: 0,
allowCalEventOverlap: true,
overlapEventsSeparate: true,
totalEventsWidthPercentInOneColumn : 100,
readonly: false,
allowEventCreation: 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) {
},
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; // NICHT
},
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:
* <ul>
* <li>%start%</li>
* <li>%end%</li>
* <li>%date%</li>
* </ul>
* @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: '<br />',
/**
* 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) {
$.ajax({
type: "POST",
url: "index.php?module=kalender&action=delete",
data: "id=" + eventId ,
dataType: "html",
success: function(msg)
{
$("#message").html(msg);
},
error: function()
{
alert("Can't save event");
}
});
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) {
$.ajax({
type: "POST",
url: "index.php?module=kalender&action=update",
data: "id=" + calEvent.id + "&title=" + calEvent.title + "&text=" + calEvent.body + "&start=" + calEvent.start.getTime() + "&end=" + calEvent.end.getTime() + "&public=" + calEvent.publicEvent,
dataType: "html",
success: function(msg){$("#message").html(msg);},
error: function(){ alert("Can't save event");}
});
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 = $.map(self.element.find('.wc-cal-event'), 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 = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').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;
if ($target.data('preventClick')) {
return;
}
var $calEvent = $target.hasClass('wc-cal-event') ? $target : $target.parents('.wc-cal-event');
if ($calEvent.length) {
freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent'));
options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
}
}).mouseover(function(event) {
var $target = $(event.target);
if (self._isDraggingOrResizing($target)) {
return;
}
if ($target.hasClass('wc-cal-event')) {
options.eventMouseover($target.data('calEvent'), $target, event);
}
}).mouseout(function(event) {
var $target = $(event.target);
if (self._isDraggingOrResizing($target)) {
return;
}
if ($target.hasClass('wc-cal-event')) {
if ($target.data('sizing')) { return;}
options.eventMouseout($target.data('calEvent'), $target, 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 = $('<div class=\"ui-widget wc-container\">').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.buttons) {
var calendarNavHtml = '';
calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
calendarNavHtml += '<div class=\"wc-display\"></div>';
calendarNavHtml += '<div class=\"wc-nav\">';
calendarNavHtml += '<button class=\"wc-prev\">' + options.buttonText.lastWeek + '</button>';
calendarNavHtml += '<button class=\"wc-today\">' + options.buttonText.today + '</button>';
calendarNavHtml += '<button class=\"wc-next\">' + options.buttonText.nextWeek + '</button>';
calendarNavHtml += '</div>';
calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
calendarNavHtml += '</div>';
$(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 = $('<input type="radio" id="' + _id + '" name="wc-switch-display" class="wc-switch-display"/>');
var _label = $('<label for="' + _id + '"></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 += '<div class=\"ui-widget-header wc-toolbar\">';
calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
calendarNavHtml += '</div>';
$(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 = '<div class=\"ui-widget-content wc-header\">';
calendarHeaderHtml += '<table><tbody><tr><td class=\"wc-time-column-header\"></td>';
for (var i = 1; i <= options.daysToShow; i++) {
calendarHeaderHtml += '<td class=\"wc-day-column-header wc-day-' + i + '\"' + colspan + '></td>';
}
calendarHeaderHtml += '<td class=\"wc-scrollbar-shim\"' + rowspan + '></td></tr>';
//users row
if (showAsSeparatedUser) {
calendarHeaderHtml += '<tr><td class=\"wc-time-column-header\"></td>';
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 += '<td class=\"' + _headerClass + ' wc-user-header wc-day-' + i + ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
// calendarHeaderHtml+= "<div class=\"wc-user-header wc-day-" + i + " wc-user-" + self._getUserIdFromIndex(j) +"\" >";
calendarHeaderHtml += self._getUserName(j);
// calendarHeaderHtml+= "</div>";
calendarHeaderHtml += '</td>';
}
}
calendarHeaderHtml += '</tr>';
}
//close the header
calendarHeaderHtml += '</tbody></table></div>';
$(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 = '<div class=\"wc-scrollable-grid\">';
$calendarBody += '<table class=\"wc-time-slots\">';
$calendarBody += '<tbody>';
$calendarBody += '</tbody>';
$calendarBody += '</table>';
$calendarBody += '</div>';
$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 = '<tr class=\"wc-grid-row-timeslot\">';
renderRow += '<td class=\"wc-grid-timeslot-header\"' + rowspan + '></td>';
renderRow += '<td colspan=\"' + options.daysToShow * (showAsSeparatedUser ? options.users.length : 1) + '\">';
renderRow += '<div class=\"wc-no-height-wrapper wc-time-slot-wrapper\">';
renderRow += '<div class=\"wc-time-slots\">';
for (i = start; i < end; i++) {
for (j = 0; j < options.timeslotsPerHour - 1; j++) {
renderRow += '<div class=\"wc-time-slot\"></div>';
}
renderRow += '<div class=\"wc-time-slot wc-hour-end\"></div>';
}
renderRow += '</div>';
renderRow += '</div>';
renderRow += '</td>';
renderRow += '</tr>';
$(renderRow).appendTo($calendarTableTbody);
},
/**
* render the odd even columns
*/
_renderCalendarBodyOddEven: function($calendarTableTbody) {
if (this.options.displayOddEven) {
var options = this.options,
renderRow = '<tr class=\"wc-grid-row-oddeven\">',
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 += '<td class=\"wc-day-column day-' + i + '\">';
renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\"></div>';
renderRow += '</div>';
renderRow += '</td>';
}
else {
var uLength = options.users.length;
for (var j = 0; j < uLength; j++) {
oddEven = (oddEven == 'odd' ? 'even' : 'odd');
renderRow += '<td class=\"wc-day-column day-' + i + '\">';
renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\" ></div>';
renderRow += '</div>';
renderRow += '</td>';
}
}
}
renderRow += '</tr>';
$(renderRow).appendTo($calendarTableTbody);
}
},
/**
* render the freebusy placeholders
*/
_renderCalendarBodyFreeBusy: function($calendarTableTbody) {
if (this.options.displayFreeBusys) {
var self = this, options = this.options,
renderRow = '<tr class=\"wc-grid-row-freebusy\">',
showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
renderRow += '</td>';
//now let's display freebusy placeholders
for (var i = 1; i <= options.daysToShow; i++) {
if (options.displayFreeBusys) {
if (!showAsSeparatedUser) {
renderRow += '<td class=\"wc-day-column day-' + i + '\">';
renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i + '\"></div>';
renderRow += '</div>';
renderRow += '</td>';
}
else {
var uLength = options.users.length;
for (var j = 0; j < uLength; j++) {
renderRow += '<td class=\"wc-day-column day-' + i + '\">';
renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i;
renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
renderRow += '</div>';
renderRow += '</div>';
renderRow += '</td>';
}
}
}
}
renderRow += '</tr>';
$(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 = '<tr class=\"wc-grid-row-events\">';
renderRow += '<td class=\"wc-grid-timeslot-header\">';
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 += '<div class=\"wc-hour-header ' + bhClass + '\">';
if (options.use24Hour) {
renderRow += '<div class=\"wc-time-header-cell\">' + self._24HourForIndex(i) + '</div>';
}
else {
renderRow += '<div class=\"wc-time-header-cell\">' + self._hourForIndex(i) + '<span class=\"wc-am-pm\">' + self._amOrPm(i) + '</span></div>';
}
renderRow += '</div>';
}
renderRow += '</td>';
//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 += '<td class=\"' + _columnBaseClass + ' wc-day-column-first wc-day-column-last day-' + i + '\">';
renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i + '\"></div>';
renderRow += '</td>';
}
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 += '<td class=\"' + _columnBaseClass + ' ' + columnclass + ' day-' + i + '\">';
renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i;
renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
renderRow += '</div>';
renderRow += '</td>';
}
}
}
renderRow += '</tr>';
$(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 = $('<div class=\"wc-cal-event wc-new-cal-event wc-new-cal-event-creating\"></div>');
$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);
}
self._trigger('beforeEventNew', event, {
'calEvent': newCalEvent,
'createdFromSingleClick': createdFromSingleClick,
'calendar': self.element
});
options.eventNew(newCalEvent, $renderedCalEvent, freeBusyManager, self.element, event);
}
});
},
/*
* 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);
},
/*
* 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 && this.options.title.length) {
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();
_title = _title.split('%start%').join(self._formatDate(_start, options.dateFormat));
_title = _title.split('%end%').join(self._formatDate(_end, options.dateFormat));
_title = _title.split('%date%').join(self._formatDate(_date, options.dateFormat));
$('.wc-toolbar .wc-title', self.element).html(_title);
}
//self._clearFreeBusys();
},
/*
* gets the calendar title options
*/
_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;
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);
//render the freebusys
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);
calEvent.end.setMinutes(0);
calEvent.end.setSeconds(0);
if (($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
self._renderEvent(calEvent, $weekDay);
}
//start is set to the begin of the new day
start.setDate(start.getDate() + 1);
start.setHours(minHour);
start.setMinutes(0);
start.setSeconds(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);
}
}
//put back the initial start date
calEvent.start = initialStart;
});
$weekDayColumns.each(function() {
self._adjustOverlappingEvents($(this));
});
options.calendarAfterLoad(self.element);
if (!eventsToRender.length) {
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, eventTyp, eventHtml, $calEventList, $modifiedEvent;
eventClass = calEvent.id ? 'wc-cal-event' : 'wc-cal-event wc-new-cal-event';
eventHtml = '<div class=\"' + eventClass + ' ui-corner-all\">';
eventHtml += '<div class=\"wc-time ui-corner-top\"></div>';
eventHtml += '<div class=\"wc-title\"></div></div>';
$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;
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);
self._refreshEventDetails(newCalEvent, $calEvent);
self._positionEvent($weekDay, $calEvent);
self._adjustOverlappingEvents($weekDay);
//trigger resize callback
options.eventResize(newCalEvent, calEvent, $calEvent);
$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) {
$calEvent.find('.wc-time').html(this.options.eventHeader(calEvent, this.element));
$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 for different representations
*/
_cleanDate: function(d) {
if (typeof d == 'string') {
// if is numeric
if (!isNaN(parseFloat(d)) && isFinite()) {
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 = '<div class="wc-freebusy"></div>';
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.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);