diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 7c9cffae..1c5801a1 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -52,6 +52,8 @@ class calendar extends rcube_plugin
'calendar_timeslots' => 2,
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
+ 'calendar_work_start' => 6,
+ 'calendar_work_end' => 18,
);
private $default_categories = array(
@@ -627,6 +629,8 @@ class calendar extends rcube_plugin
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
+ $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
+ $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['timezone'] = $this->timezone;
// localization
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ed0318e4..bfa2614d 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -42,9 +42,10 @@ function rcube_calendar_ui(settings)
var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0);
var day_clicked = day_clicked_ts = 0;
var ignore_click = false;
+ var event_defaults = { free_busy:'busy' };
var event_attendees = null;
var attendees_list;
- var freebusy_ui = {};
+ var freebusy_ui = { workinhoursonly:false };
var freebusy_data = {};
var freebusy_needsupdate;
@@ -330,7 +331,8 @@ function rcube_calendar_ui(settings)
var $dialog = $("#eventedit");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
- me.selected_event = $.extend({}, event); // clone event object
+ me.selected_event = $.extend(event_defaults, event); // clone event object (with defaults)
+ event = me.selected_event; // change reference to clone
freebusy_needsupdate = false;
// reset dialog first, enable/disable fields according to editable state
@@ -616,27 +618,34 @@ function rcube_calendar_ui(settings)
return false;
// set form elements
- var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
- var startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
- var starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
- var enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
- var endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
+ var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
+ freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
+ freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
+ freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
+ freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
if (allday.checked) {
starttime.val("00:00").hide();
endtime.val("23:59").hide();
+ event.allDay = true;
}
+ // read attendee roles from drop-downs
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (event_attendees[i])
+ event_attendees[i].role = $(elem).val();
+ });
+
// render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date();
- fb_start.setTime(Math.max(now, event.start));
+ fb_start.setTime(event.start);
fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + 86400000);
freebusy_data = {};
freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
- freebusy_ui.numdays = allday.checked ? 7 : 1;
+ freebusy_ui.numdays = allday.checked ? 7 : Math.ceil(duration * 2 / 86400);
freebusy_ui.interval = allday.checked ? 360 : 60;
freebusy_ui.start = fb_start;
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + 86400000 * freebusy_ui.numdays);
@@ -653,9 +662,23 @@ function rcube_calendar_ui(settings)
$('#schedule-attendees-list').html(list_html);
+ // enable/disable buttons
+ $('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
+
// dialog buttons
var buttons = {};
+ buttons[rcmail.gettext('adobt', 'calendar')] = function() {
+ $('#edit-startdate').val(freebusy_ui.startdate.val());
+ $('#edit-starttime').val(freebusy_ui.starttime.val());
+ $('#edit-enddate').val(freebusy_ui.enddate.val());
+ $('#edit-endtime').val(freebusy_ui.endtime.val());
+ if (freebusy_needsupdate)
+ update_freebusy_status(me.selected_event);
+ freebusy_needsupdate = false;
+ $dialog.dialog("close");
+ };
+
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
@@ -666,6 +689,8 @@ function rcube_calendar_ui(settings)
closeOnEscape: true,
title: rcmail.gettext('scheduletime', 'calendar'),
close: function() {
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide();
},
buttons: buttons,
@@ -673,6 +698,10 @@ function rcube_calendar_ui(settings)
width: 850
}).show();
+ // hide edit dialog on IE6 because of drop-down elements
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','hidden');
+
// adjust dialog size to fit grid without scrolling
var gridw = $('#schedule-freebusy-times').width();
var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
@@ -704,8 +733,8 @@ function rcube_calendar_ui(settings)
lastdate = datestr;
}
- // TODO: define working hours by config
- css = (freebusy_ui.numdays == 1 && (curdate.getHours() < 6 || curdate.getHours() > 18)) ? 'offhours' : 'workinghours';
+ // set css class according to working hours
+ css = (freebusy_ui.numdays == 1 && (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end'])) ? 'offhours' : 'workinghours';
times_row += '
' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + ' | ';
slots_row += ' | ';
@@ -749,15 +778,15 @@ function rcube_calendar_ui(settings)
{
var overlay = $('#schedule-event-time');
if (me.selected_event.end.getTime() < freebusy_ui.start.getTime() || me.selected_event.start.getTime() > freebusy_ui.end.getTime()) {
- overlay.hide();
+ overlay.draggable('disable').hide();
}
else {
var table = $('#schedule-freebusy-times'),
width = 0,
pos = { top:table.children('thead').height(), left:0 },
- eventstart = Math.floor(me.selected_event.start.getTime() / 1000),
- eventend = Math.floor(me.selected_event.end.getTime() / 1000),
- slotstart = Math.floor(freebusy_ui.start.getTime() / 1000),
+ eventstart = date2unixtime(me.selected_event.start),
+ eventend = date2unixtime(me.selected_event.end),
+ slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60,
slotend, fraction, $cell;
@@ -782,14 +811,38 @@ function rcube_calendar_ui(settings)
width = table.width() - pos.left;
// overlay is visible
- if (width > 0)
- overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show();
+ if (width > 0) {
+ overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).draggable('enable').show();
+
+ // configure draggable
+ if (!overlay.data('isdraggable')) {
+ overlay.draggable({
+ axis: 'x',
+ scroll: true,
+ stop: function(e, ui){
+ // convert pixels to time
+ var px = ui.position.left;
+ var range_p = $('#schedule-freebusy-times').width();
+ var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
+ var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
+ newstart.setSeconds(0); newstart.setMilliseconds(0);
+ // round to 5 minutes
+ var round = newstart.getMinutes() % 5;
+ if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
+ else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
+ // update event times
+ update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
+ }
+ }).data('isdraggable', true);
+ }
+ }
else
- overlay.hide();
+ overlay.draggable('disable').hide();
}
};
+
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
@@ -851,6 +904,109 @@ function rcube_calendar_ui(settings)
});
}
};
+
+ // write changed event date/times back to form fields
+ var update_freebusy_dates = function(start, end)
+ {
+ me.selected_event.start = start;
+ me.selected_event.end = end;
+ freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
+ freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
+ freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
+ freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
+ freebusy_needsupdate = true;
+ };
+
+ // attempt to find a time slot where all attemdees are available
+ var freebusy_find_slot = function(dir)
+ {
+ var event = me.selected_event,
+ eventstart = date2unixtime(event.start), // calculate with unitimes
+ eventend = date2unixtime(event.end),
+ duration = eventend - eventstart,
+ sinterval = freebusy_data.interval * 60,
+ intvlslots = event.allDay ? 4 : 1,
+ numslots = Math.ceil(duration / sinterval),
+ checkdate, slotend, email, curdate;
+
+ // shift event times to next possible slot
+ eventstart += sinterval * intvlslots * dir;
+ eventend += sinterval * intvlslots * dir;
+
+ // iterate through free-busy slots and find candidates
+ var candidatecount = 0, candidatestart = candidateend = success = false;
+ for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) {
+ slotend = slot + sinterval;
+ if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
+ continue;
+
+ // respect workingours setting
+ if (freebusy_ui.workinhoursonly && freebusy_data.interval <= 60) {
+ curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration));
+ if (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end']) { // skip off-hours
+ candidatestart = candidateend = false;
+ candidatecount = 0;
+ continue;
+ }
+ }
+
+ if (!candidatestart)
+ candidatestart = slot;
+
+ // check freebusy data for all attendees
+ for (var i=0; i < event_attendees.length; i++) {
+ if ((email = event_attendees[i].email) && freebusy_data[email][slot] > 1) {
+ candidatestart = candidateend = false;
+ break;
+ }
+ }
+
+ // occupied slot
+ if (!candidatestart) {
+ slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
+ candidatecount = 0;
+ continue;
+ }
+
+ // set candidate end to slot end time
+ candidatecount++;
+ if (dir < 0 && !candidateend)
+ candidateend = slotend;
+
+ // if candidate is big enough, this is it!
+ if (candidatecount == numslots) {
+ if (dir > 0) {
+ event.start = fromunixtime(candidatestart);
+ event.end = fromunixtime(candidatestart + duration);
+ }
+ else {
+ event.end = fromunixtime(candidateend);
+ event.start = fromunixtime(candidateend - duration);
+ }
+ success = true;
+ break;
+ }
+ }
+
+ // update event date/time display
+ if (success) {
+ update_freebusy_dates(event.start, event.end);
+
+ // move freebusy grid if necessary
+ if (event.start.getTime() >= freebusy_ui.end.getTime())
+ render_freebusy_grid(1);
+ else if (event.end.getTime() <= freebusy_ui.start.getTime())
+ render_freebusy_grid(-1);
+ else
+ render_freebusy_overlay();
+
+ var now = new Date();
+ $('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
+ }
+ else {
+ alert(rcmail.gettext('noslotfound','calendar'));
+ }
+ };
// update event properties and attendees availability if event times have changed
@@ -858,6 +1014,7 @@ function rcube_calendar_ui(settings)
{
if (me.selected_event) {
var allday = $('#edit-allday').get(0);
+ me.selected_event.allDay = allday.checked;
me.selected_event.start = parse_datetime(allday.checked ? '00:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
me.selected_event.end = parse_datetime(allday.checked ? '23:59' : $('#edit-endtime').val(), $('#edit-enddate').val());
if (event_attendees)
@@ -1701,7 +1858,11 @@ function rcube_calendar_ui(settings)
$('#shedule-freebusy-prev').html(bw.ie6 ? '<<' : '◄').button().click(function(){ render_freebusy_grid(-1); });
$('#shedule-freebusy-next').html(bw.ie6 ? '>>' : '►').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
+ $('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
+ $('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
+
$('#schedule-freebusy-wokinghours').click(function(){
+ freebusy_ui.workinhoursonly = this.checked;
$('#workinghourscss').remove();
if (this.checked)
$('').appendTo('head');
diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist
index aa580071..a6b23415 100644
--- a/plugins/calendar/config.inc.php.dist
+++ b/plugins/calendar/config.inc.php.dist
@@ -52,6 +52,12 @@ $rcmail_config['calendar_first_day'] = 1;
// first hour of the calendar (0-23)
$rcmail_config['calendar_first_hour'] = 6;
+// working hours begin
+$rcmail_config['calendar_work_start'] = 6;
+
+// working hours end
+$rcmail_config['calendar_work_end'] = 18;
+
// event categories
$rcmail_config['calendar_categories'] = array(
'Personal' => 'c0c0c0',
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 74e1228c..07eb7b0a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -591,7 +591,7 @@ class calendar_ui
$table->add('attendees',
html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) .
html::div('timesheader', ' ') .
- html::div(array('id' => 'schedule-attendees-list'), '')
+ html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
);
$table->add('times',
html::div('scroll',
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index ab4bf5c4..4b405ee5 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -31,6 +31,7 @@ $labels['edit'] = 'Edit';
$labels['save'] = 'Save';
$labels['remove'] = 'Remove';
$labels['cancel'] = 'Cancel';
+$labels['adobt'] = 'Adopt changes';
$labels['print'] = 'Print calendars';
$labels['title'] = 'Summary';
$labels['description'] = 'Description';
@@ -99,7 +100,10 @@ $labels['availtentative'] = 'Tentative';
$labels['availoutofoffice'] = 'Out of Office';
$labels['scheduletime'] = 'Available times';
$labels['sendnotifications'] = 'Send notifications';
-$labels['onlyworkinghours'] = 'Show only working hours';
+$labels['onlyworkinghours'] = 'Only working hours';
+$labels['prevslot'] = 'Previous Slot';
+$labels['nextslot'] = 'Next Slot';
+$labels['noslotfound'] = 'Unable to find a free time slot';
// event dialog tabs
$labels['tabsummary'] = 'Summary';
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css
index aeb8a9ec..64669a92 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/default/calendar.css
@@ -597,6 +597,7 @@ td.topalign {
#edit-attendees-legend {
margin-top: 3em;
+ margin-bottom: 0.5em;
}
#edit-attendees-legend .legend {
@@ -667,13 +668,33 @@ td.topalign {
border-color: #ccc;
}
-#schedule-attendees-list div.attendee {
- padding: 3px 20px 3px 6px;
+.attendees-list .attendee {
+ padding: 3px 4px 3px 20px;
+ background: url('images/attendee-status.gif') 2px -97px no-repeat;
+}
+
+.attendees-list div.attendee {
border-top: 1px solid #ccc;
}
-#schedule-attendees-list div.loading {
- background: url('images/loading-small.gif') top right no-repeat;
+.attendees-list span.attendee {
+ margin-right: 2em;
+}
+
+.attendees-list .organizer {
+ background-position: 3px -77px;
+}
+
+.attendees-list .opt-participant {
+ background-position: 2px -117px;
+}
+
+.attendees-list .chair {
+ background-position: 2px -137px;
+}
+
+.attendees-list .loading {
+ background: url('images/loading-small.gif') 1px 50% no-repeat;
}
#schedule-freebusy-times {
@@ -706,11 +727,12 @@ td.topalign {
background: rgba(60, 60, 60, 0.6);
opacity: 0.5;
border-radius: 4px;
+ cursor: move;
}
#eventfreebusy .schedule-options {
position: relative;
- margin-bottom: 2em;
+ margin-bottom: 1.5em;
}
#eventfreebusy .schedule-buttons {
@@ -719,6 +741,15 @@ td.topalign {
right: 0;
}
+#eventfreebusy .schedule-find-buttons {
+ padding-bottom:0.5em;
+}
+
+#eventfreebusy .schedule-find-buttons button {
+ min-width: 9em;
+ text-align: center;
+}
+
span.edit-alarm-set {
white-space: nowrap;
}
diff --git a/plugins/calendar/skins/default/images/attendee-status.gif b/plugins/calendar/skins/default/images/attendee-status.gif
index a5f65a02..5c08aae3 100644
Binary files a/plugins/calendar/skins/default/images/attendee-status.gif and b/plugins/calendar/skins/default/images/attendee-status.gif differ
diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html
index bf83f030..de0419cc 100644
--- a/plugins/calendar/skins/default/templates/calendar.html
+++ b/plugins/calendar/skins/default/templates/calendar.html
@@ -188,25 +188,42 @@
-