From ad5a5c6e84008a374c4973edc50c5087d572ee3b Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 24 Jul 2011 12:46:54 +0200 Subject: [PATCH 1/4] Implement finding of free time slots --- plugins/calendar/calendar.php | 4 + plugins/calendar/calendar_ui.js | 139 ++++++++++++++++-- plugins/calendar/config.inc.php.dist | 6 + plugins/calendar/lib/calendar_ui.php | 2 +- plugins/calendar/localization/en_US.inc | 4 + plugins/calendar/skins/default/calendar.css | 38 ++++- .../skins/default/images/attendee-status.gif | Bin 1611 -> 2041 bytes .../skins/default/templates/calendar.html | 16 +- 8 files changed, 193 insertions(+), 16 deletions(-) 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..6147da40 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -44,7 +44,7 @@ function rcube_calendar_ui(settings) var ignore_click = false; var event_attendees = null; var attendees_list; - var freebusy_ui = {}; + var freebusy_ui = { workinhoursonly:false }; var freebusy_data = {}; var freebusy_needsupdate; @@ -628,9 +628,15 @@ function rcube_calendar_ui(settings) endtime.val("23:59").hide(); } + // 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); @@ -653,9 +659,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(startdate.val()); + $('#edit-starttime').val(starttime.val()); + $('#edit-enddate').val(enddate.val()); + $('#edit-endtime').val(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 +686,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 +695,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 +730,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 += ' '; @@ -754,10 +780,10 @@ function rcube_calendar_ui(settings) 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), + pos = { top:table.children('thead').height(), left:-1 }, + 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,7 +808,7 @@ function rcube_calendar_ui(settings) width = table.width() - pos.left; // overlay is visible - if (width > 0) + if (width > 0 && pos.left >= 0) overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show(); else overlay.hide(); @@ -852,6 +878,97 @@ function rcube_calendar_ui(settings) } }; + // 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, + numslots = Math.ceil(duration / sinterval), + checkdate, slotend, email, curdate; + + // shift event times to next possible slot + eventstart += sinterval * dir; + eventend += sinterval * dir; + + 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 = false; + candidatecount = 0; + break; + } + } + + // occupied slot + if (!candidatestart) + 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 fields + if (success) { + $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])); + $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])); + $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); + $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])); + freebusy_needsupdate = true; + + // 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 var event_times_changed = function() @@ -1701,7 +1818,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..81dc57bb 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'; @@ -100,6 +101,9 @@ $labels['availoutofoffice'] = 'Out of Office'; $labels['scheduletime'] = 'Available times'; $labels['sendnotifications'] = 'Send notifications'; $labels['onlyworkinghours'] = 'Show 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..4dcf1011 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 { @@ -719,6 +740,15 @@ td.topalign { right: 0; } +#eventfreebusy .schedule-find-buttons { + padding-top:1em; +} + +#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 a5f65a02e14602aae4db5d4c2955bce679af565a..5c08aae316ad60f3b71dd178c6e0e922943888b1 100644 GIT binary patch literal 2041 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
- - +
+
  @@ -205,8 +205,20 @@  
+
+
+ + +
+
+
+ + + + +
From 2cefdc33434aac893ef970a91330303bd6a384d4 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 24 Jul 2011 15:00:35 +0200 Subject: [PATCH 2/4] Fix/improve free time slot selection --- plugins/calendar/calendar_ui.js | 21 ++++++----- plugins/calendar/localization/en_US.inc | 2 +- plugins/calendar/skins/default/calendar.css | 4 +-- .../skins/default/templates/calendar.html | 35 +++++++++++-------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 6147da40..7a4af37f 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -626,6 +626,7 @@ function rcube_calendar_ui(settings) if (allday.checked) { starttime.val("00:00").hide(); endtime.val("23:59").hide(); + event.allDay = true; } // read attendee roles from drop-downs @@ -642,7 +643,7 @@ function rcube_calendar_ui(settings) 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); @@ -780,7 +781,7 @@ function rcube_calendar_ui(settings) else { var table = $('#schedule-freebusy-times'), width = 0, - pos = { top:table.children('thead').height(), left:-1 }, + pos = { top:table.children('thead').height(), left:0 }, eventstart = date2unixtime(me.selected_event.start), eventend = date2unixtime(me.selected_event.end), slotstart = date2unixtime(freebusy_ui.start), @@ -808,7 +809,7 @@ function rcube_calendar_ui(settings) width = table.width() - pos.left; // overlay is visible - if (width > 0 && pos.left >= 0) + 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(); else overlay.hide(); @@ -886,12 +887,13 @@ function rcube_calendar_ui(settings) 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 * dir; - eventend += sinterval * dir; + eventstart += sinterval * intvlslots * dir; + eventend += sinterval * intvlslots * dir; 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) { @@ -915,15 +917,17 @@ function rcube_calendar_ui(settings) // 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 = false; - candidatecount = 0; + candidatestart = candidateend = false; break; } } // occupied slot - if (!candidatestart) + if (!candidatestart) { + slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir; + candidatecount = 0; continue; + } // set candidate end to slot end time candidatecount++; @@ -975,6 +979,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) diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 81dc57bb..4b405ee5 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -100,7 +100,7 @@ $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'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 4dcf1011..36e42a4e 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -731,7 +731,7 @@ td.topalign { #eventfreebusy .schedule-options { position: relative; - margin-bottom: 2em; + margin-bottom: 1.5em; } #eventfreebusy .schedule-buttons { @@ -741,7 +741,7 @@ td.topalign { } #eventfreebusy .schedule-find-buttons { - padding-top:1em; + padding-bottom:0.5em; } #eventfreebusy .schedule-find-buttons button { diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index 8654baaa..de0419cc 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -188,27 +188,32 @@
- +  
-
-
- -   - +
+
+ +   + +
+
+ +   + +
-
- -   - -
-
-
- - +
+
+ + +
+
+ +

From 99adbe4c763935f5b4e024880e0e76515cb59db1 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 24 Jul 2011 15:53:25 +0200 Subject: [PATCH 3/4] Make event time overlay draggable --- plugins/calendar/calendar_ui.js | 71 +++++++++++++++------ plugins/calendar/skins/default/calendar.css | 1 + 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 7a4af37f..e28800b2 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -616,12 +616,12 @@ 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(); @@ -667,10 +667,10 @@ function rcube_calendar_ui(settings) var buttons = {}; buttons[rcmail.gettext('adobt', 'calendar')] = function() { - $('#edit-startdate').val(startdate.val()); - $('#edit-starttime').val(starttime.val()); - $('#edit-enddate').val(enddate.val()); - $('#edit-endtime').val(endtime.val()); + $('#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; @@ -776,7 +776,7 @@ 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'), @@ -809,14 +809,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) { @@ -878,6 +902,18 @@ 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) @@ -895,6 +931,7 @@ function rcube_calendar_ui(settings) 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; @@ -949,13 +986,9 @@ function rcube_calendar_ui(settings) } } - // update event date/time fields + // update event date/time display if (success) { - $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])); - $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])); - $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); - $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])); - freebusy_needsupdate = true; + update_freebusy_dates(event.start, event.end); // move freebusy grid if necessary if (event.start.getTime() >= freebusy_ui.end.getTime()) diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 36e42a4e..64669a92 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -727,6 +727,7 @@ td.topalign { background: rgba(60, 60, 60, 0.6); opacity: 0.5; border-radius: 4px; + cursor: move; } #eventfreebusy .schedule-options { From 3dfccb89e1fcb0d8a7552dd5334b89556a375150 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 25 Jul 2011 09:39:25 +0200 Subject: [PATCH 4/4] Set show-as busy as default for new events --- plugins/calendar/calendar_ui.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index e28800b2..bfa2614d 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -42,6 +42,7 @@ 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 = { workinhoursonly:false }; @@ -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