').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).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 = $('
');
_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).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).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);
$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).appendTo($calendarTableTbody);
},
/**
* render the odd even columns
*/
_renderCalendarBodyOddEven: function($calendarTableTbody) {
if (this.options.displayOddEven) {
var options = this.options,
renderRow = '
';
$(renderRow).appendTo($calendarTableTbody);
}
},
/**
* render the freebusy placeholders
*/
_renderCalendarBodyFreeBusy: function($calendarTableTbody) {
if (this.options.displayFreeBusys) {
var self = this, options = this.options,
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).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 self = this;
var d = MyDate(),
options = this.options,
businessHours = options.businessHours;
self._scrollToHour(d.getHours() ,false);
// 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
if (!calEvent || !calEvent.start || !calEvent.end) return;
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);
if (self._hourLineTimeout) {
clearInterval(self._hourLineTimeout);
self._hourLineTimeout = false;
}
if (options.hourLine) {
self._drawCurrentHourLine();
self._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 capabilities 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;
}
}
//scroll to the hour plus some padding so that hour is in middle of viewport
var hourHeaderHeight = this.element.find(".wc-grid-timeslot-header .wc-hour-header").outerHeight();
var calHeight = this.element.find(".wc-scrollable-grid").outerHeight();
var scroll = (hourHeaderHeight * slot) - calHeight/3;
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 = MyDate();
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) {
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() {
$(this).attr('unselectable', 'on')
.css({
'-moz-user-select': '-moz-none',
'-moz-user-select': 'none',
'-o-user-select': 'none',
'-khtml-user-select': 'none', /* you could also put this in a class */
'-webkit-user-select': 'none',/* and add the CSS class here instead */
'-ms-user-select': 'none',
'user-select': 'none'
}).bind('selectstart', 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
if (d[d.length - 1] !== 'Z') d += 'Z';
var o = new Date(d);
o.setTime(o.getTime() + (o.getTimezoneOffset() * 60 * 1000));
return o;
}
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 $placeholder = $(this);
var s = self._cloneDate($placeholder.data('startDate')),
e = self._cloneDate(s);
s.setHours(start);
e.setHours(end);
$placeholder.find('.wc-freebusy').remove();
$.each($placeholder.data('wcFreeBusyManager').getFreeBusys(s, e), function() {
self._renderFreeBusy(this, $placeholder);
});
});
}
},
/**
* 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) {
if (!freebusy) return;
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;
},
/**
* retrieves the first freebusy manager matching demand.
*/
getFreeBusyManagersFor: function(date, users) {
var calEvent = {
start: date,
end: date
};
this._setEventUserId(calEvent, users);
return this.getFreeBusyManagerForEvent(calEvent);
},
/**
* retrieves the first freebusy manager for given event.
*/
getFreeBusyManagerForEvent: function(newCalEvent) {
var self = this,
options = this.options,
freeBusyManager;
if (options.displayFreeBusys) {
var $freeBusyPlaceHolders = 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];
}
$freeBusyPlaceHolders.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,
$freeBusyPlaceHolders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
_freeBusys = self._cleanFreeBusys(freeBusys);
$.each(_freeBusys, function(index, _freeBusy) {
var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHolders);
//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 [optional] if you do not pass end date, will return the freeBusy within which this date falls.
* @param {Date} end [optional] 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 inserted 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);