summaryrefslogtreecommitdiffstats
path: root/modules-available/js_weekcalendar
diff options
context:
space:
mode:
authorSimon Rettberg2017-07-25 19:02:01 +0200
committerSimon Rettberg2017-07-25 19:02:01 +0200
commitbb53f6136e2950f3d656728be469d318a0d9f606 (patch)
tree4e8648ff6fde4b8496eba6e19afdfd7bf542c241 /modules-available/js_weekcalendar
parent[locationinfo] Better error handling in hisinone backend (diff)
downloadslx-admin-bb53f6136e2950f3d656728be469d318a0d9f606.tar.gz
slx-admin-bb53f6136e2950f3d656728be469d318a0d9f606.tar.xz
slx-admin-bb53f6136e2950f3d656728be469d318a0d9f606.zip
[locationinfo] Make panel accessible via slxadmin, add URL type panel
Diffstat (limited to 'modules-available/js_weekcalendar')
-rwxr-xr-xmodules-available/js_weekcalendar/clientscript.js2968
-rwxr-xr-xmodules-available/js_weekcalendar/style.css284
2 files changed, 3252 insertions, 0 deletions
diff --git a/modules-available/js_weekcalendar/clientscript.js b/modules-available/js_weekcalendar/clientscript.js
new file mode 100755
index 00000000..28b9e3cf
--- /dev/null
+++ b/modules-available/js_weekcalendar/clientscript.js
@@ -0,0 +1,2968 @@
+/*
+ * 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/
+ */
+var startdate = 0;
+
+function SetUpDate(d) {
+ startdate = d.getTime() - new Date().getTime();
+}
+
+/**
+ *
+ * @return {Date}
+ */
+function MyDate() {
+ return new Date(startdate + new Date().getTime());
+}
+
+(function($) {
+
+ // check the jquery version
+ var _v = $.fn.jquery.split('.'),
+ _jQuery14OrLower = (10 * _v[0] + _v[1]) < 15;
+ $.widget('ui.weekCalendar', (function() {
+ var _currentAjaxCall, _hourLineTimeout;
+
+ return {
+ options: {
+ date: MyDate(),
+ timeFormat: null,
+ dateFormat: 'M d, Y',
+ alwaysDisplayTimeMinutes: true,
+ use24Hour: false,
+ daysToShow: 7,
+ minBodyHeight: 100,
+ firstDayOfWeek: function(calendar) {
+ if ($(calendar).weekCalendar('option', 'daysToShow') != 5) {
+ return 0;
+ } else {
+ //workweek
+ return 1;
+ }
+ }, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday
+ useShortDayNames: false,
+ timeSeparator: ' to ',
+ startParam: 'start',
+ endParam: 'end',
+ businessHours: {start: 8, end: 18, limitDisplay: false},
+ newEventText: 'New Event',
+ timeslotHeight: 20,
+ defaultEventLength: 2,
+ timeslotsPerHour: 4,
+ minDate: null,
+ maxDate: null,
+ showHeader: true,
+ buttons: true,
+ buttonText: {
+ today: 'today',
+ lastWeek: 'previous',
+ nextWeek: 'next'
+ },
+ switchDisplay: {},
+ scrollToHourMillis: 500,
+ allowEventDelete: false,
+ allowCalEventOverlap: false,
+ overlapEventsSeparate: false,
+ totalEventsWidthPercentInOneColumn: 100,
+ readonly: false,
+ allowEventCreation: true,
+ hourLine: false,
+ deletable: function(calEvent, element) {
+ return true;
+ },
+ draggable: function(calEvent, element) {
+ return true;
+ },
+ resizable: function(calEvent, element) {
+ return true;
+ },
+ eventClick: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ },
+ eventRender: function(calEvent, element) {
+ return element;
+ },
+ eventAfterRender: function(calEvent, element) {
+ return element;
+ },
+ eventRefresh: function(calEvent, element) {
+ return element;
+ },
+ eventDrag: function(calEvent, element) {
+ },
+ eventDrop: function(calEvent, element) {
+ },
+ eventResize: function(calEvent, element) {
+ },
+ eventNew: function(calEvent, element, dayFreeBusyManager,
+ calendar, mouseupEvent) {
+ },
+ eventMouseover: function(calEvent, $event) {
+ },
+ eventMouseout: function(calEvent, $event) {
+ },
+ eventDelete: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ calendar.weekCalendar('removeEvent',calEvent.id);
+ },
+ calendarBeforeLoad: function(calendar) {
+ },
+ calendarAfterLoad: function(calendar) {
+ },
+ noEvents: function() {
+ },
+ eventHeader: function(calEvent, calendar) {
+ var options = calendar.weekCalendar('option');
+ var one_hour = 3600000;
+ var displayTitleWithTime = calEvent.end.getTime() - calEvent.start.getTime() <= (one_hour / options.timeslotsPerHour);
+ if (displayTitleWithTime) {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ ': ' + calEvent.title;
+ } else {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ options.timeSeparator +
+ calendar.weekCalendar(
+ 'formatTime', calEvent.end);
+ }
+ },
+ eventBody: function(calEvent, calendar) {
+ return calEvent.title;
+ },
+ shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ /* multi-users options */
+ /**
+ * the available users for calendar.
+ * if you want to display users separately, enable the
+ * showAsSeparateUsers option.
+ * if you provide a list of user and do not enable showAsSeparateUsers
+ * option, then only the events that belongs to one or several of
+ * given users will be displayed
+ * @type {array}
+ */
+ users: [],
+ /**
+ * should the calendar be displayed with separate column for each
+ * users.
+ * note that this option does nothing if you do not provide at least
+ * one user.
+ * @type {boolean}
+ */
+ showAsSeparateUsers: true,
+ /**
+ * callback used to read user id from a user object.
+ * @param {Object} user the user to retrieve the id from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {int|String} the user id.
+ */
+ getUserId: function(user, index, calendar) {
+ return index;
+ },
+ /**
+ * callback used to read user name from a user object.
+ * @param {Object} user the user to retrieve the name from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {String} the user name.
+ */
+ getUserName: function(user, index, calendar) {
+ return user;
+ },
+ /**
+ * reads the id(s) of user(s) for who the event should be displayed.
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getEventUserId: function(calEvent, calendar) {
+ return calEvent.userId;
+ },
+ /**
+ * sets user id(s) to the calEvent
+ * @param {Object} calEvent the calEvent to set informations to.
+ * @param {jQuery} calendar the calendar object.
+ * @return {Object} the calEvent with modified user id.
+ */
+ setEventUserId: function(userId, calEvent, calendar) {
+ calEvent.userId = userId;
+ return calEvent;
+ },
+ /* freeBusy options */
+ /**
+ * should the calendar display freebusys ?
+ * @type {boolean}
+ */
+ displayFreeBusys: false,
+ /**
+ * read the id(s) for who the freebusy is available
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getFreeBusyUserId: function(calFreeBusy, calendar) {
+ return calFreeBusy.userId;
+ },
+ /**
+ * the default freeBusy object, used to manage default state
+ * @type {Object}
+ */
+ defaultFreeBusy: {free: false},
+ /**
+ * function used to display the freeBusy element
+ * @type {Function}
+ * @param {Object} freeBusy the freeBusy timeslot to render.
+ * @param {jQuery} $freeBusy the freeBusy HTML element.
+ * @param {jQuery} calendar the calendar element.
+ */
+ freeBusyRender: function(freeBusy, $freeBusy, calendar) {
+ if (!freeBusy.free) {
+ $freeBusy.addClass('free-busy-busy');
+ }
+ else {
+ $freeBusy.addClass('free-busy-free');
+ }
+ return $freeBusy;
+ },
+ /* other options */
+ /**
+ * true means start on first day of week, false means starts on
+ * startDate.
+ * @param {jQuery} calendar the calendar object.
+ * @type {Function|bool}
+ */
+ startOnFirstDayOfWeek: function(calendar) {
+ return $(calendar).weekCalendar('option', 'daysToShow') >= 5;
+ },
+ /**
+ * should the columns be rendered alternatively using odd/even
+ * class
+ * @type {boolean}
+ */
+ displayOddEven: false,
+ textSize: 13,
+ /**
+ * the title attribute for the calendar. possible placeholders are:
+ * <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'));
+ },
+
+ resizeCalendar:function(){
+ this._resizeCalendar();
+ },
+ scrollToHour:function(){
+ this._scrollToHour(MyDate().getHours(), false);
+ },
+ /*
+ * Clear all events currently loaded into the calendar
+ */
+ clear: function() {
+ this._clearCalendar();
+ },
+
+ /*
+ * Go to this week
+ */
+ today: function() {
+ this._clearCalendar();
+ this._loadCalEvents(MyDate());
+ },
+
+ /*
+ * Go to the previous week relative to the currently displayed week
+ */
+ prevWeek: function() {
+ //minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() - (MILLIS_IN_WEEK / 6));
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Go to the next week relative to the currently displayed week
+ */
+ nextWeek: function() {
+ //add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() + MILLIS_IN_WEEK + MILLIS_IN_DAY);
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoWeek: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoDate: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /**
+ * change the number of days to show
+ */
+ setDaysToShow: function(daysToShow) {
+ var self = this;
+ var hour = self._getCurrentScrollHour();
+ self.options.daysToShow = daysToShow;
+ $(self.element).html('');
+ self._renderCalendar();
+ self._loadCalEvents();
+ self._resizeCalendar();
+ self._scrollToHour(hour, false);
+
+ if (this.options.resizeEvent) {
+ $(window).unbind(this.options.resizeEvent);
+ $(window).bind(this.options.resizeEvent, function() {
+ self._resizeCalendar();
+ });
+ }
+ },
+
+ /*
+ * Remove an event based on it's id
+ */
+ removeEvent: function(eventId) {
+
+ var self = this;
+
+ self.element.find('.wc-cal-event').each(function() {
+ if ($(this).data('calEvent').id === eventId) {
+ $(this).remove();
+ return false;
+ }
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * Removes any events that have been added but not yet saved (have no id).
+ * This is useful to call after adding a freshly saved new event.
+ */
+ removeUnsavedEvents: function() {
+
+ var self = this;
+
+ self.element.find('.wc-new-cal-event').each(function() {
+ $(this).remove();
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * update an event in the calendar. If the event exists it refreshes
+ * it's rendering. If it's a new event that does not exist in the calendar
+ * it will be added.
+ */
+ updateEvent: function(calEvent) {
+ this._updateEventInCalendar(calEvent);
+ },
+
+ /*
+ * Returns an array of timeslot start and end times based on
+ * the configured grid of the calendar. Returns in both date and
+ * formatted time based on the 'timeFormat' config option.
+ */
+ getTimeslotTimes: function(date) {
+ var options = this.options;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed);
+
+ var times = [],
+ startMillis = startDate.getTime();
+ for (var i = 0; i < options.timeslotsPerDay; i++) {
+ var endMillis = startMillis + options.millisPerTimeslot;
+ times[i] = {
+ start: new Date(startMillis),
+ startFormatted: this.formatTime(new Date(startMillis), options.timeFormat),
+ end: new Date(endMillis),
+ endFormatted: this.formatTime(new Date(endMillis), options.timeFormat)
+ };
+ startMillis = endMillis;
+ }
+ return times;
+ },
+
+ formatDate: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else {
+ return this._formatDate(date, this.options.dateFormat);
+ }
+ },
+
+ formatTime: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else if (this.options.timeFormat) {
+ return this._formatDate(date, this.options.timeFormat);
+ } else if (this.options.use24Hour) {
+ return this._formatDate(date, 'H:i');
+ } else {
+ return this._formatDate(date, 'h:i a');
+ }
+ },
+
+ serializeEvents: function() {
+ var self = this;
+ var calEvents = [];
+
+ self.element.find('.wc-cal-event').each(function() {
+ calEvents.push($(this).data('calEvent'));
+ });
+ return calEvents;
+ },
+
+ next: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.nextWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() + this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ prev: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.prevWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() - this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+ getCurrentFirstDay: function() {
+ return this._dateFirstDayOfWeek(this.options.date || MyDate());
+ },
+ getCurrentLastDay: function() {
+ return this._addDays(this.getCurrentFirstDay(), this.options.daysToShow - 1);
+ },
+
+ /*********************
+ * private functions *
+ *********************/
+ _setOption: function(key, value) {
+ var self = this;
+ if (self.options[key] != value) {
+ // event callback change, no need to re-render the events
+ if (key == 'beforeEventNew') {
+ self.options[key] = value;
+ return;
+ }
+
+ // this could be made more efficient at some stage by caching the
+ // events array locally in a store but this should be done in conjunction
+ // with a proper binding model.
+
+ var currentEvents = self.element.find('.wc-cal-event').map(function() {
+ return $(this).data('calEvent');
+ });
+
+ var newOptions = {};
+ newOptions[key] = value;
+ self._renderEvents({events: currentEvents, options: newOptions}, self.element.find('.wc-day-column-inner'));
+ }
+ },
+
+ // compute dynamic options based on other config values
+ _computeOptions: function() {
+ var options = this.options;
+ if (options.businessHours.limitDisplay) {
+ options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start);
+ options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 3600000; // 60 * 60 * 1000
+ options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay;
+ } else {
+ options.timeslotsPerDay = options.timeslotsPerHour * 24;
+ options.millisToDisplay = MILLIS_IN_DAY;
+ options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay;
+ }
+ },
+
+ /*
+ * Resize the calendar scrollable height based on the provided function in options.
+ */
+ _resizeCalendar: function() {
+ var options = this.options;
+ if (options && $.isFunction(options.height)) {
+ var calendarHeight = options.height(this.element);
+ var headerHeight = this.element.find('.wc-header').outerHeight(true);
+ var navHeight = this.element.find('.wc-toolbar').outerHeight(true);
+ var scrollContainerHeight = Math.max(calendarHeight - navHeight - headerHeight, options.minBodyHeight);
+ var timeslotHeight = this.element.find('.wc-time-slots').outerHeight(true);
+ 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;
+
+ // click is disabled
+ if ($target.data('preventClick')) {
+ return;
+ }
+
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent'));
+
+ if (options.allowEventDelete && $target.hasClass('wc-cal-event-delete')) {
+ options.eventDelete($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ } else {
+ options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ }
+ }).mouseover(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseover($calEvent.data('calEvent'), $calEvent, event);
+ }).mouseout(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseout($calEvent.data('calEvent'), $calEvent, event);
+ });
+ },
+
+ /**
+ * check if a ui draggable or resizable is currently being dragged or
+ * resized.
+ */
+ _isDraggingOrResizing: function($target) {
+ return $target.hasClass('ui-draggable-dragging') ||
+ $target.hasClass('ui-resizable-resizing');
+ },
+
+ /*
+ * Render the main calendar layout
+ */
+ _renderCalendar: function() {
+ var $calendarContainer, $weekDayColumns;
+ var self = this;
+ var options = this.options;
+
+ $calendarContainer = $('<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.showHeader ) return;
+ 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);
+ }
+
+ 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(
+ $('<div>', {
+ '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 = '<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 || ui.helper;
+ options.eventDrag(calEvent, $calEvent);
+ }
+ });
+ },
+
+ /*
+ * Add droppable capabilites to weekdays to allow dropping of calEvents only
+ */
+ _addDroppableToWeekDay: function($weekDay) {
+ var self = this;
+ var options = this.options;
+ $weekDay.droppable({
+ accept: '.wc-cal-event',
+ drop: function(event, ui) {
+ var $calEvent = ui.draggable;
+ var top = Math.round(parseInt(ui.position.top));
+ var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top);
+ var calEvent = $calEvent.data('calEvent');
+ var newCalEvent = $.extend(true, {}, calEvent, {start: eventDuration.start, end: eventDuration.end});
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ if (showAsSeparatedUser) {
+ // we may have dragged the event on column with a new user.
+ // nice way to handle that is:
+ // - get the newly dragged on user
+ // - check if user is part of the event
+ // - if yes, nothing changes, if not, find the old owner to remove it and add new one
+ var newUserId = $weekDay.data('wcUserId');
+ var userIdList = self._getEventUserId(calEvent);
+ var oldUserId = $(ui.draggable.parents('.wc-day-column-inner').get(0)).data('wcUserId');
+ if (!$.isArray(userIdList)) {
+ userIdList = [userIdList];
+ }
+ if ($.inArray(newUserId, userIdList) == -1) {
+ // remove old user
+ var _index = $.inArray(oldUserId, userIdList);
+ userIdList.splice(_index, 1);
+ // add new user ?
+ if ($.inArray(newUserId, userIdList) == -1) {
+ userIdList.push(newUserId);
+ }
+ }
+ newCalEvent = self._setEventUserId(newCalEvent, ((userIdList.length == 1) ? userIdList[0] : userIdList));
+ }
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true);
+ var $weekDayColumns = self.element.find('.wc-day-column-inner');
+
+ //trigger drop callback
+ options.eventDrop(newCalEvent, calEvent, $calEvent);
+
+ var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns));
+ $calEvent.hide();
+
+ $calEvent.data('preventClick', true);
+
+ var $weekDayOld = self._findWeekDayForEvent($calEvent.data('calEvent'), self.element.find('.wc-time-slots .wc-day-column-inner'));
+
+ if ($weekDayOld.data('startDate') != $weekDay.data('startDate')) {
+ self._adjustOverlappingEvents($weekDayOld);
+ }
+ self._adjustOverlappingEvents($weekDay);
+
+ setTimeout(function() {
+ $calEvent.remove();
+ }, 1000);
+
+ }
+ });
+ },
+
+ /*
+ * Add resizable capabilities to a calEvent
+ */
+ _addResizableToCalEvent: function(calEvent, $calEvent, $weekDay) {
+ var self = this;
+ var options = this.options;
+ $calEvent.resizable({
+ grid: options.timeslotHeight,
+ containment: $weekDay,
+ handles: 's',
+ minHeight: options.timeslotHeight,
+ stop: function(event, ui) {
+ var $calEvent = ui.element;
+ var newEnd = new Date($calEvent.data('calEvent').start.getTime() + Math.max(1, Math.round(ui.size.height / options.timeslotHeight)) * options.millisPerTimeslot);
+ if (self._needDSTdayShift($calEvent.data('calEvent').start, newEnd))
+ newEnd = self._getDSTdayShift(newEnd, -1);
+ var newCalEvent = $.extend(true, {}, calEvent, {start: calEvent.start, end: newEnd});
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent);
+
+ //trigger resize callback
+ options.eventResize(newCalEvent, calEvent, $calEvent);
+ self._refreshEventDetails(newCalEvent, $calEvent);
+ self._positionEvent($weekDay, $calEvent);
+ self._adjustOverlappingEvents($weekDay);
+ $calEvent.data('preventClick', true);
+ setTimeout(function() {
+ $calEvent.removeData('preventClick');
+ }, 500);
+ }
+ });
+ $('.ui-resizable-handle', $calEvent).text('=');
+ },
+
+ /*
+ * Refresh the displayed details of a calEvent in the calendar
+ */
+ _refreshEventDetails: function(calEvent, $calEvent) {
+ var suffix = '';
+ if (!this.options.readonly &&
+ this.options.allowEventDelete &&
+ this.options.deletable(calEvent,$calEvent)) {
+ suffix = '<div class="wc-cal-event-delete ui-icon ui-icon-close"></div>';
+ }
+ $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 $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) {
+ 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;
+ },
+
+ /**
+ * retrives the first freebusy manager matching demand.
+ */
+ getFreeBusyManagersFor: function(date, users) {
+ var calEvent = {
+ start: date,
+ end: date
+ };
+ this._setEventUserId(calEvent, users);
+ return this.getFreeBusyManagerForEvent(calEvent);
+ },
+ /**
+ * retrives the first freebusy manager for given event.
+ */
+ getFreeBusyManagerForEvent: function(newCalEvent) {
+ var self = this,
+ options = this.options,
+ freeBusyManager;
+ if (options.displayFreeBusys) {
+ var $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ freeBusy = new FreeBusy({start: newCalEvent.start, end: newCalEvent.end}),
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ userId = showAsSeparatedUser ? self._getEventUserId(newCalEvent) : null;
+ if (!$.isArray(userId)) {
+ userId = [userId];
+ }
+ $freeBusyPlaceHoders.each(function() {
+ var manager = $(this).data('wcFreeBusyManager'),
+ has_overlap = manager.isWithin(freeBusy.getEnd()) ||
+ manager.isWithin(freeBusy.getEnd()) ||
+ freeBusy.isWithin(manager.getStart()) ||
+ freeBusy.isWithin(manager.getEnd());
+ if (has_overlap && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), userId) != -1)) {
+ freeBusyManager = $(this).data('wcFreeBusyManager');
+ return false;
+ }
+ });
+ }
+ return freeBusyManager;
+ },
+ /**
+ * appends the freebusys to replace the old ones.
+ * @param {array|object} freeBusys freebusy(s) to apply.
+ */
+ updateFreeBusy: function(freeBusys) {
+ var self = this,
+ options = this.options;
+ if (options.displayFreeBusys) {
+ var $toRender,
+ $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ _freeBusys = self._cleanFreeBusys(freeBusys);
+
+ $.each(_freeBusys, function(index, _freeBusy) {
+
+ var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHoders);
+ //if freebusy has a placeholder
+ if ($weekdays && $weekdays.length) {
+ $weekdays.each(function(index, day) {
+ var manager = $(day).data('wcFreeBusyManager');
+ manager.insertFreeBusy(_freeBusy);
+ $(day).data('wcFreeBusyManager', manager);
+ });
+ $toRender = $toRender ? $toRender.add($weekdays) : $weekdays;
+ }
+ });
+ self._refreshFreeBusys($toRender);
+ }
+ },
+
+ /* NEW OPTIONS MANAGEMENT */
+
+ /**
+ * checks wether or not the calendar should be displayed starting on first day of week
+ */
+ _startOnFirstDayOfWeek: function() {
+ return jQuery.isFunction(this.options.startOnFirstDayOfWeek) ? this.options.startOnFirstDayOfWeek(this.element) : this.options.startOnFirstDayOfWeek;
+ },
+
+ /**
+ * finds out the current scroll to apply it when changing the view
+ */
+ _getCurrentScrollHour: function() {
+ var self = this;
+ var options = this.options;
+ var $scrollable = this.element.find('.wc-scrollable-grid');
+ var scroll = $scrollable.scrollTop();
+ if (self.options.businessHours.limitDisplay) {
+ scroll = scroll + options.businessHours.start * options.timeslotHeight * options.timeslotsPerHour;
+ }
+ return Math.round(scroll / (options.timeslotHeight * options.timeslotsPerHour)) + 1;
+ },
+ _getJsonOptions: function() {
+ if ($.isFunction(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions(this.element));
+ }
+ if ($.isPlainObject(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions);
+ }
+ return {};
+ },
+ _getHeaderDate: function(date) {
+ var options = this.options;
+ if (options.getHeaderDate && $.isFunction(options.getHeaderDate))
+ {
+ return options.getHeaderDate(date, this.element);
+ }
+ var dayName = options.useShortDayNames ? options.shortDays[date.getDay()] : options.longDays[date.getDay()];
+ return dayName + (options.headerSeparator) + this._formatDate(date, options.dateFormat);
+ },
+
+
+
+ /**
+ * returns corrected date related to DST problem
+ */
+ _getDSTdayShift: function(date, shift) {
+ var start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0);
+ var offset1 = start.getTimezoneOffset();
+ var offset2 = date.getTimezoneOffset();
+ if (offset1 == offset2)
+ return date;
+ shift = shift ? shift : 1;
+ return new Date(date.getTime() - shift * (offset1 > offset2 ? -1 : 1) * (Math.max(offset1, offset2) - Math.min(offset1, offset2)) * 60000);
+ },
+ _needDSTdayShift: function(date1, date2) {
+ return date1.getTimezoneOffset() != date2.getTimezoneOffset();
+ }
+
+
+
+ }; // end of widget function return
+ })() //end of widget function closure execution
+ ); // end of $.widget("ui.weekCalendar"...
+
+ $.extend($.ui.weekCalendar, {
+ version: '2.0-dev',
+ updateLayoutOptions: {
+ startOnFirstDayOfWeek: true,
+ firstDayOfWeek: true,
+ daysToShow: true,
+ displayOddEven: true,
+ timeFormat: true,
+ dateFormat: true,
+ use24Hour: true,
+ useShortDayNames: true,
+ businessHours: true,
+ timeslotHeight: true,
+ timeslotsPerHour: true,
+ buttonText: true,
+ height: true,
+ shortMonths: true,
+ longMonths: true,
+ shortDays: true,
+ longDays: true,
+ textSize: true,
+ users: true,
+ showAsSeparateUsers: true,
+ displayFreeBusys: true
+ }
+ });
+
+ var MILLIS_IN_DAY = 86400000;
+ var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
+
+ /* FREE BUSY MANAGERS */
+ var FreeBusyProto = {
+ getStart: function() {return this.getOption('start')},
+ getEnd: function() {return this.getOption('end')},
+ getOption: function() {
+ if (!arguments.length) { return this.options }
+ if (typeof(this.options[arguments[0]]) !== 'undefined') {
+ return this.options[arguments[0]];
+ }
+ else if (typeof(arguments[1]) !== 'undefined') {
+ return arguments[1];
+ }
+ return null;
+ },
+ setOption: function(key, value) {
+ if (arguments.length == 1) {
+ $.extend(this.options, arguments[0]);
+ return this;
+ }
+ this.options[key] = value;
+ return this;
+ },
+ isWithin: function(dateTime) {return Math.floor(dateTime.getTime() / 1000) >= Math.floor(this.getStart().getTime() / 1000) && Math.floor(dateTime.getTime() / 1000) < Math.floor(this.getEnd().getTime() / 1000)},
+ isValid: function() {return this.getStart().getTime() < this.getEnd().getTime()}
+ };
+
+ /**
+ * @constructor
+ * single user freebusy manager.
+ */
+ var FreeBusy = function(options) {
+ this.options = $.extend({}, options || {});
+ };
+ $.extend(FreeBusy.prototype, FreeBusyProto);
+
+ var FreeBusyManager = function(options) {
+ this.options = $.extend({
+ defaultFreeBusy: {}
+ }, options || {});
+ this.freeBusys = [];
+ this.freeBusys.push(new FreeBusy($.extend({
+ start: this.getStart(),
+ end: this.getEnd()
+ }, this.options.defaultFreeBusy)));
+ };
+ $.extend(FreeBusyManager.prototype, FreeBusyProto, {
+ /**
+ * return matching freeBusys.
+ * if you do not pass any argument, returns all freebusys.
+ * if you only pass a start date, only matchinf freebusy will be returned.
+ * if you pass 2 arguments, then all freebusys available within the time period will be returned
+ * @param {Date} start [optionnal] if you do not pass end date, will return the freeBusy within which this date falls.
+ * @param {Date} end [optionnal] the date where to stop the search.
+ * @return {Array} an array of FreeBusy matching arguments.
+ */
+ getFreeBusys: function() {
+ switch (arguments.length) {
+ case 0:
+ return this.freeBusys;
+ case 1:
+ var freeBusy = [];
+ var start = arguments[0];
+ if (!this.isWithin(start)) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.isWithin(start)) {
+ freeBusy.push(this);
+ }
+ if (Math.floor(this.getEnd().getTime() / 1000) > Math.floor(start.getTime() / 1000)) {
+ return false;
+ }
+ });
+ return freeBusy;
+ default:
+ //we assume only 2 first args are revealants
+ var freeBusy = [];
+ var start = arguments[0], end = arguments[1];
+ var tmpFreeBusy = new FreeBusy({start: start, end: end});
+ if (end.getTime() < start.getTime() || this.getStart().getTime() > end.getTime() || this.getEnd().getTime() < start.getTime()) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.getStart().getTime() >= end.getTime()) {
+ return false;
+ }
+ if (tmpFreeBusy.isWithin(this.getStart()) && tmpFreeBusy.isWithin(this.getEnd())) {
+ freeBusy.push(this);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.isWithin(tmpFreeBusy.getEnd())) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.getStart().getTime() < start.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getEnd()) && this.getEnd().getTime() > end.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ freeBusy.push(_f);
+ }
+ });
+ return freeBusy;
+ }
+ },
+ insertFreeBusy: function(freeBusy) {
+ var freeBusy = new FreeBusy(freeBusy.getOption());
+ //first, if inserted freebusy is bigger than manager
+ if (freeBusy.getStart().getTime() < this.getStart().getTime()) {
+ freeBusy.setOption('start', this.getStart());
+ }
+ if (freeBusy.getEnd().getTime() > this.getEnd().getTime()) {
+ freeBusy.setOption('end', this.getEnd());
+ }
+ var start = freeBusy.getStart(), end = freeBusy.getEnd(),
+ startIndex = 0, endIndex = this.freeBusys.length - 1,
+ newFreeBusys = [];
+ var pushNewFreeBusy = function(_f) {if (_f.isValid()) newFreeBusys.push(_f);};
+
+ $.each(this.freeBusys, function(index) {
+ //within the loop, we have following vars:
+ // curFreeBusyItem: the current iteration freeBusy, part of manager freeBusys list
+ // start: the insterted freeBusy start
+ // end: the inserted freebusy end
+ var curFreeBusyItem = this;
+ if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.isWithin(end)) {
+ /*
+ we are in case where inserted freebusy fits in curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-------------*
+ obviously, start and end indexes are this item.
+ */
+ startIndex = index;
+ endIndex = index;
+ if (start.getTime() == curFreeBusyItem.getStart().getTime() && end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case, inserted freebusy is exactly curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-----------------------------*
+
+ just replace curFreeBusyItem with freeBusy.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ }
+ else if (start.getTime() == curFreeBusyItem.getStart().getTime()) {
+ /*
+ in this case inserted freebusy starts with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ var _f2 = new FreeBusy(curFreeBusyItem.getOption());
+ _f2.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else if (end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case inserted freebusy ends with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else {
+ /*
+ in this case inserted freebusy is within curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ var _f3 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ _f3.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ pushNewFreeBusy(_f3);
+ }
+ /*
+ as work is done, no need to go further.
+ return false
+ */
+ return false;
+ }
+ else if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.getEnd().getTime() != start.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set start index AND insert before part, we'll insert freebusy later
+ */
+ if (curFreeBusyItem.getStart().getTime() != start.getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ pushNewFreeBusy(_f1);
+ }
+ startIndex = index;
+ }
+ else if (curFreeBusyItem.isWithin(end) && curFreeBusyItem.getStart().getTime() != end.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set end index AND insert freebusy AND insert after part if needed
+ */
+ pushNewFreeBusy(new FreeBusy(freeBusy.getOption()));
+ if (end.getTime() < curFreeBusyItem.getEnd().getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ }
+ endIndex = index;
+ return false;
+ }
+ });
+ //now compute arguments
+ var tmpFB = this.freeBusys;
+ this.freeBusys = [];
+
+ if (startIndex) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(0, startIndex));
+ }
+ this.freeBusys = this.freeBusys.concat(newFreeBusys);
+ if (endIndex < tmpFB.length) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(endIndex + 1));
+ }
+/* if(start.getDate() == 1){
+ console.info('insert from '+freeBusy.getStart() +' to '+freeBusy.getEnd());
+ console.log('index from '+ startIndex + ' to ' + endIndex);
+ var str = [];
+ $.each(tmpFB, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' : 'busy'))});
+ console.log(str.join('\n'));
+
+ console.log('insert');
+ var str = [];
+ $.each(newFreeBusys, function(i){str.push(this.getStart().getHours() + ' > ' + this.getEnd().getHours())});
+ console.log(str.join(', '));
+
+ console.log('results');
+ var str = [];
+ $.each(this.freeBusys, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' :'busy'))});
+ console.log(str.join('\n'));
+ }*/
+ return this;
+ }
+ });
+})(jQuery);
+
diff --git a/modules-available/js_weekcalendar/style.css b/modules-available/js_weekcalendar/style.css
new file mode 100755
index 00000000..aae8f956
--- /dev/null
+++ b/modules-available/js_weekcalendar/style.css
@@ -0,0 +1,284 @@
+.wc-container {
+ font-size: 14px;
+ font-family: arial, helvetica;
+}
+
+.wc-toolbar {
+ padding: 1em;
+ font-size:0.8em;
+}
+
+.wc-toolbar .wc-nav {
+ float:left;
+}
+
+.wc-toolbar .wc-display {
+ float: right;
+}
+
+.wc-toolbar button {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.wc-toolbar .wc-title {
+ text-align: center;
+ padding:0;
+ margin:0;
+}
+
+.wc-container table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+.wc-container table td {
+ margin: 0;
+ padding: 0;
+}
+
+.wc-header {
+ background: #eee;
+ border-width:1px 0;
+ border-style:solid;
+}
+.wc-header table{
+ width: 100%;
+ table-layout:fixed;
+}
+
+.wc-grid-timeslot-header,
+.wc-header .wc-time-column-header {
+ width: 45px;
+}
+
+.wc-header .wc-scrollbar-shim {
+ width: 16px;
+}
+
+.wc-header .wc-day-column-header {
+ text-align: center;
+ padding: 0.4em;
+}
+
+.wc-header .wc-user-header{
+ text-align: center;
+ padding: 0.4em 0;
+ overflow:hidden;
+}
+.wc-grid-timeslot-header {
+ background: #eee;
+}
+
+.wc-scrollable-grid {
+ overflow: auto;
+ overflow-x: hidden !important;
+ overflow-y: auto !important;
+ position: relative;
+ background-color: #fff;
+ width: 100%;
+}
+
+
+table.wc-time-slots {
+ width: 100%;
+ table-layout: fixed;
+ cursor: default;
+ overflow:hidden;
+}
+
+.wc-day-column {
+ width: 13.5%;
+ overflow: visible;
+ vertical-align: top;
+}
+.wc-day-column-header{border-width: 0 0 1px 3px; border-style: solid;border-color:transparent;}
+.wc-scrollable-grid .wc-day-column-last,
+.wc-scrollable-grid .wc-day-column-middle{border-width: 0 0 0 1px; border-style: dashed;}
+.wc-scrollable-grid .wc-day-column-first{border-width: 0 0 0 3px; border-style: double;}
+
+.wc-day-column-inner {
+ width: 100%;
+ position:relative;
+}
+
+.wc-no-height-wrapper{
+ position:relative;
+ overflow: visible;
+ height: 0px;
+}
+
+.wc-time-slot-wrapper {
+/* top: 3px;*/
+}
+.wc-oddeven-wrapper .wc-full-height-column{
+/* top: 2px; */
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+.wc-freebusy-wrapper .wc-freebusy{
+/* top: 1px;*/
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+
+.wc-time-slots {
+ position: absolute;
+ width: 100%;
+}
+
+.wc-column-odd,
+.wc-column-even.ui-state-hover{background-image:none;border:none;}
+
+.wc-header .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.wc-day-column-header{border-width:0 3px; border-style: solid;}
+.wc-header .wc-user-header{border-width:0;}
+
+.wc-time-slots .wc-day-column.ui-state-default{background:transparent;}
+.wc-time-slots .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.ui-state-active.wc-day-column-middle{border-width:0;}
+.wc-header .wc-today.ui-state-active.wc-day-column-first{border-left-width:3px;}
+.wc-header .wc-today.ui-state-active.wc-day-column-last{border-right-width:3px;}
+
+.wc-full-height-column{
+ display:block;
+/* width:100%;*/
+}
+
+
+.wc-time-header-cell {
+ padding: 5px;
+ height: 80px; /* reference height */
+}
+
+
+.wc-time-slot {
+ border-bottom: 1px dotted #ddd;
+}
+
+.wc-hour-header {
+ text-align: right;
+}
+.wc-hour-header.ui-state-active,
+.wc-hour-header.ui-state-default{
+ border-width:0 0 1px 0;
+}
+
+.wc-hour-end, .wc-hour-header {
+ border-bottom: 1px solid #ccc;
+ color: #555;
+}
+
+.wc-business-hours {
+ background-color: #E6EEF1;
+ border-bottom: 1px solid #ccc;
+ color: #333;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+.wc-business-hours .wc-am-pm {
+ font-size: 0.6em;
+}
+
+.wc-day-header-cell {
+ text-align: center;
+ vertical-align: middle;
+ padding: 5px;
+}
+
+
+
+.wc-time-slot-header .wc-header-cell {
+ text-align: right;
+ padding-right: 10px;
+}
+
+.wc-cal-event {
+ background-color: #68a1e5;
+ /* Modern Browsers */ opacity: 0.8;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
+ /* IE 5-7 */ filter: alpha(opacity=80);
+ /* Netscape */ -moz-opacity: 0.8;
+ /* Safari 1 */ -khtml-opacity: 0.8;
+ position: absolute;
+ text-align: center;
+ overflow: hidden;
+ cursor: pointer;
+ color: #fff;
+ width: 100%;
+ display: none;
+}
+
+
+.wc-cal-event-delete {
+ float: right;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+
+.wc-cal-event.ui-resizable-resizing {
+ cursor: s-resize;
+}
+
+.wc-cal-event .wc-time {
+ background-color: #2b72d0;
+ border: 1px solid #1b62c0;
+ color: #fff;
+ padding: 0;
+ font-weight: bold;
+}
+
+.wc-container .ui-draggable .wc-time {
+ cursor: move;
+}
+
+.wc-cal-event .wc-title {
+ position: relative;
+}
+
+.wc-container .ui-resizable-s {
+ height: 10px;
+ line-height: 10px;
+ bottom: -2px;
+ font-size: .75em;
+}
+
+
+.wc-container .ui-draggable-dragging {
+ z-index: 1000;
+}
+
+.free-busy-free{}
+.free-busy-busy{
+ background:url("./libs/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png") repeat scroll 50% 50% #666666;
+}
+
+/** hourLine */
+
+.wc-hourline {
+ height: 0pt;
+ border-top: 2px solid #FF7F6E;
+ overflow: hidden;
+ position: absolute;
+ width: inherit;
+}
+
+/* IE6 hacks */
+* html .wc-no-height-wrapper{position:absolute;}
+* html .wc-time-slot-wrapper{top:3px;}
+* html .wc-grid-row-oddeven{top:2px;}
+* html .wc-grid-row-freebusy{top:1px;}
+
+/* IE7 hacks */
+*:first-child+html .wc-no-height-wrapper{position:relative;}
+*:first-child+html .wc-time-slot-wrapper{top:3px;}
+*:first-child+html .wc-grid-row-oddeven{top:2px;}
+*:first-child+html .wc-grid-row-freebusy{top:1px;}
+*:first-child+html .wc-time-slots .wc-today{/* due to rendering issues, no background */background:none;}