From d575d09fd57a6b87ed633d619f3f44a1c05a10a9 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Tue, 18 Oct 2016 09:20:13 +0200 Subject: [PATCH] Consider event duration and calendar_timeslots setting in availability finder (#5469, T1381) Summary: 1. "Previous Slot" action moves the selection box to the beginning of the time slot not the end. It looked to me as an obvious mistake. 2. Both "previous slot" and "next slot" move the selection box in intervals defined in calendar_timeslots option instead of one hour. I.e. it will be one hour, 30 minutes, 20 minutes, 15 minutes or 10 minutes, depending what's configured in Preferences > Calendar > Time slots per hour. Reviewers: #roundcube_kolab_plugins_developers Differential Revision: https://git.kolab.org/D207 --- plugins/calendar/calendar.php | 40 +++-- plugins/calendar/calendar_ui.js | 188 +++++++++++++--------- plugins/calendar/skins/larry/calendar.css | 37 +++-- 3 files changed, 159 insertions(+), 106 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 1e2fc91f..f27b3351 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -2185,6 +2185,7 @@ class calendar extends rcube_plugin // if the backend has free-busy information $fblist = $this->driver->get_freebusy_list($email, $start, $end); + if (is_array($fblist)) { $status = 'FREE'; @@ -2234,13 +2235,26 @@ class calendar extends rcube_plugin $dts = new DateTime('@'.$start); $dts->setTimezone($this->timezone); } - + $fblist = $this->driver->get_freebusy_list($email, $start, $end); - $slots = array(); - + $slots = ''; + + // prepare freebusy list before use (for better performance) + if (is_array($fblist)) { + foreach ($fblist as $idx => $slot) { + list($from, $to, ) = $slot; + + // check for possible all-day times + if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { + // shift into the user's timezone for sane matching + $fblist[$idx][0] -= $this->gmt_offset; + $fblist[$idx][1] -= $this->gmt_offset; + } + } + } + // build a list from $start till $end with blocks representing the fb-status for ($s = 0, $t = $start; $t <= $end; $s++) { - $status = self::FREEBUSY_UNKNOWN; $t_end = $t + $interval * 60; $dt = new DateTime('@'.$t); $dt->setTimezone($this->timezone); @@ -2248,16 +2262,10 @@ class calendar extends rcube_plugin // determine attendee's status if (is_array($fblist)) { $status = self::FREEBUSY_FREE; + foreach ($fblist as $slot) { list($from, $to, $type) = $slot; - // check for possible all-day times - if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { - // shift into the user's timezone for sane matching - $from -= $this->gmt_offset; - $to -= $this->gmt_offset; - } - if ($from < $t_end && $to > $t) { $status = isset($type) ? $type : self::FREEBUSY_BUSY; if ($status == self::FREEBUSY_BUSY) // can't get any worse :-) @@ -2265,9 +2273,12 @@ class calendar extends rcube_plugin } } } - - $slots[$s] = $status; - $times[$s] = intval($dt->format($strformat)); + else { + $status = self::FREEBUSY_UNKNOWN; + } + + // use most compact format, assume $status is one digit/character + $slots .= $status; $t = $t_end; } @@ -2283,7 +2294,6 @@ class calendar extends rcube_plugin 'end' => $dte->format('c'), 'interval' => $interval, 'slots' => $slots, - 'times' => $times, )); exit; } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index ce5db2bd..8f87a6bb 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -1238,7 +1238,7 @@ function rcube_calendar_ui(settings) freebusy_data = { required:{}, all:{} }; freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400)); - freebusy_ui.interval = allday.checked ? 1440 : 60; + freebusy_ui.interval = allday.checked ? 1440 : (60 / (settings.timeslots || 1)); freebusy_ui.start = fb_start; freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays); render_freebusy_grid(0); @@ -1345,7 +1345,7 @@ function rcube_calendar_ui(settings) // adjust dialog size to fit grid without scrolling var gridw = $('#schedule-freebusy-times').width(); - var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1; + var overflow = gridw - $('#attendees-freebusy-table td.times').width(); me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow)); // fetch data from server @@ -1375,10 +1375,12 @@ function rcube_calendar_ui(settings) var lastdate, datestr, css, curdate = new Date(), allday = (freebusy_ui.interval == 1440), + interval = allday ? 1440 : (freebusy_ui.interval * (settings.timeslots || 1)); times_css = (allday ? 'allday ' : ''), dates_row = '', times_row = '', slots_row = ''; + for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) { curdate.setTime(t); datestr = fc.fullCalendar('formatDate', curdate, date_format); @@ -1391,9 +1393,9 @@ function rcube_calendar_ui(settings) // set css class according to working hours css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours'; times_row += '' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + ''; - slots_row += ' '; + slots_row += ' '; - t += freebusy_ui.interval * 60000; + t += interval * 60000; } dates_row += ''; times_row += ''; @@ -1407,7 +1409,7 @@ function rcube_calendar_ui(settings) } // add line for all/required attendees - times_html += ' '; + times_html += ''; times_html += '' + slots_row + ''; var table = $('#schedule-freebusy-times'); @@ -1429,7 +1431,7 @@ function rcube_calendar_ui(settings) update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000)); render_freebusy_overlay(); } - }) + }); } // if we have loaded free-busy data, show it @@ -1460,38 +1462,40 @@ function rcube_calendar_ui(settings) overlay.draggable('disable'); } else { - var table = $('#schedule-freebusy-times'), + var i, n, table = $('#schedule-freebusy-times'), width = 0, pos = { top:table.children('thead').height(), left:0 }, eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)), eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60, slotstart = date2unixtime(freebusy_ui.start), slotsize = freebusy_ui.interval * 60, - slotend, fraction, $cell; - - // iterate through slots to determine position and size of the overlay - table.children('thead').find('td').each(function(i, cell){ - slotend = slotstart + slotsize - 1; - // event starts in this slot: compute left - if (eventstart >= slotstart && eventstart <= slotend) { - fraction = 1 - (slotend - eventstart) / slotsize; - pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction); - } - // event ends in this slot: compute width - if (eventend >= slotstart && eventend <= slotend) { - fraction = 1 - (slotend - eventend) / slotsize; - width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left; - } + slotnum = freebusy_ui.interval > 60 ? 1 : (60 / freebusy_ui.interval), + cells = table.children('thead').find('td'), + cell_width = cells.first().get(0).offsetWidth, + slotend; - slotstart = slotstart + slotsize; - }); + // iterate through slots to determine position and size of the overlay + for (i=0; i < cells.length; i++) { + for (n=0; n < slotnum; n++) { + slotend = slotstart + slotsize - 1; + // event starts in this slot: compute left + if (eventstart >= slotstart && eventstart <= slotend) { + pos.left = Math.round(i * cell_width + (cell_width / slotnum) * n); + } + // event ends in this slot: compute width + if (eventend >= slotstart && eventend <= slotend) { + width = Math.round(i * cell_width + (cell_width / slotnum) * (n + 1)) - pos.left; + } + slotstart += slotsize; + } + } if (!width) 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(); + overlay.css({ width: (width-4)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show(); // configure draggable if (!overlay.data('isdraggable')) { @@ -1514,6 +1518,7 @@ function rcube_calendar_ui(settings) } else { // round to 5 minutes + // @TODO: round to timeslots? 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); @@ -1531,10 +1536,8 @@ function rcube_calendar_ui(settings) else overlay.draggable('disable').hide(); } - }; - - + // fetch free-busy information for each attendee from server var load_freebusy_data = function(from, interval) { @@ -1561,9 +1564,9 @@ function rcube_calendar_ui(settings) success: function(data) { freebusy_ui.loading--; - // find attendee - var attendee = null; - for (var i=0; i < event_attendees.length; i++) { + // find attendee + var i, attendee = null; + for (i=0; i < event_attendees.length; i++) { if (freebusy_ui.attendees[i].email == data.email) { attendee = freebusy_ui.attendees[i]; break; @@ -1571,25 +1574,31 @@ function rcube_calendar_ui(settings) } // copy data to member var - var ts, req = attendee.role != 'OPT-PARTICIPANT'; - freebusy_data.start = parseISO8601(data.start); + var ts, status, + req = attendee.role != 'OPT-PARTICIPANT', + start = parseISO8601(data.start); + + freebusy_data.start = new Date(start); + freebusy_data.end = parseISO8601(data.end); + freebusy_data.interval = data.interval; freebusy_data[data.email] = {}; - for (var i=0; i < data.slots.length; i++) { - ts = data.times[i] + ''; - freebusy_data[data.email][ts] = data.slots[i]; + + for (i=0; i < data.slots.length; i++) { + ts = date2timestring(start, data.interval > 60); + status = data.slots.charAt(i); + freebusy_data[data.email][ts] = status + start = new Date(start.getTime() + data.interval * 60000); // set totals if (!freebusy_data.required[ts]) freebusy_data.required[ts] = [0,0,0,0]; if (req) - freebusy_data.required[ts][data.slots[i]]++; + freebusy_data.required[ts][status]++; if (!freebusy_data.all[ts]) freebusy_data.all[ts] = [0,0,0,0]; - freebusy_data.all[ts][data.slots[i]]++; + freebusy_data.all[ts][status]++; } - freebusy_data.end = parseISO8601(data.end); - freebusy_data.interval = data.interval; // hide loading indicator var domid = String(data.email).replace(rcmail.identifier_expr, ''); @@ -1652,27 +1661,51 @@ function rcube_calendar_ui(settings) if (fbdata && fbdata[ts] !== undefined && row.length) { t = freebusy_ui.start.getTime(); - row.children().each(function(i, cell){ - curdate.setTime(t); - ts = date2timestring(curdate, dateonly); - cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown'); + row.children().each(function(i, cell) { + var j, n, attr, last, all_slots = [], slots = [], + all_cell = rowall.get(i), + cnt = dateonly ? 1 : (60 / freebusy_ui.interval), + percent = (100 / cnt); - // also update total row if all data was loaded - if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) { - var workinghours = cell.className.indexOf('workinghours') >= 0; - var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown'; - req_status = freebusy_data.required[ts][2] ? 'busy' : 'free'; - for (var j=1; j < status_classes.length; j++) { - if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired) - req_status = status_classes[j]; - if (freebusy_data.all[ts][j] == event_attendees.length) - all_status = status_classes[j]; + for (n=0; n < cnt; n++) { + curdate.setTime(t); + ts = date2timestring(curdate, dateonly); + attr = { + 'style': 'float:left; width:' + percent.toFixed(2) + '%', + 'class': fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown' + }; + + slots.push($('
').attr(attr)); + + // also update total row if all data was loaded + if (!freebusy_ui.loading && freebusy_data.all[ts] && all_cell) { + var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown', + req_status = freebusy_data.required[ts][2] ? 'busy' : 'free'; + + for (j=1; j < status_classes.length; j++) { + if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired) + req_status = status_classes[j]; + if (freebusy_data.all[ts][j] == event_attendees.length) + all_status = status_classes[j]; + } + + attr['class'] = req_status + ' all-' + all_status; + + // these elements use some specific styling, so we want to minimize their number + if (last && last.attr('class') == attr['class']) + last.css('width', (percent + parseFloat(last.css('width').replace('%', ''))).toFixed(2) + '%'); + else { + last = $('
').attr(attr); + all_slots.push(last); + } } - - cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status; + + t += freebusy_ui.interval * 60000; } - - t += freebusy_ui.interval * 60000; + + $(cell).html('').append(slots); + if (all_slots.length) + $(all_cell).html('').append(all_slots); }); } }; @@ -1712,17 +1745,20 @@ function rcube_calendar_ui(settings) sinterval = freebusy_data.interval * 60000, intvlslots = 1, numslots = Math.ceil(duration / sinterval), - checkdate, slotend, email, ts, slot, slotdate = new Date(); + fb_start = freebusy_data.start.getTime(), + fb_end = freebusy_data.end.getTime(), + checkdate, slotend, email, ts, slot, slotdate = new Date(), + candidatecount = 0, candidatestart = false, success = false; // 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 (slot = dir > 0 ? freebusy_data.start.getTime() : freebusy_data.end.getTime() - sinterval; - (dir > 0 && slot < freebusy_data.end.getTime()) || (dir < 0 && slot >= freebusy_data.start.getTime()); - slot += sinterval * dir) { + for (slot = dir > 0 ? fb_start : fb_end - sinterval; + (dir > 0 && slot < fb_end) || (dir < 0 && slot >= fb_start); + slot += sinterval * dir + ) { slotdate.setTime(slot); // fix slot if just crossed a DST change if (event.allDay) { @@ -1734,10 +1770,10 @@ function rcube_calendar_ui(settings) if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip continue; - // respect workingours setting + // respect workinghours setting if (freebusy_ui.workinhoursonly) { if (is_weekend(slotdate) || (freebusy_data.interval <= 60 && !is_workinghour(slotdate))) { // skip off-hours - candidatestart = candidateend = false; + candidatestart = false; candidatecount = 0; continue; } @@ -1746,11 +1782,12 @@ function rcube_calendar_ui(settings) if (!candidatestart) candidatestart = slot; - // check freebusy data for all attendees ts = date2timestring(slotdate, freebusy_data.interval > 60); + + // check freebusy data for all attendees for (var i=0; i < event_attendees.length; i++) { if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][ts] > 1) { - candidatestart = candidateend = false; + candidatestart = false; break; } } @@ -1761,22 +1798,15 @@ function rcube_calendar_ui(settings) candidatecount = 0; continue; } + else if (dir < 0) + candidatestart = slot; - // 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.setTime(candidatestart); - event.end.setTime(candidatestart + duration); - } - else { - event.end.setTime(candidateend); - event.start.setTime(candidateend - duration); - } + event.start.setTime(candidatestart); + event.end.setTime(candidatestart + duration); success = true; break; } @@ -1806,7 +1836,6 @@ function rcube_calendar_ui(settings) } }; - // update event properties and attendees availability if event times have changed var event_times_changed = function() { @@ -1821,7 +1850,6 @@ function rcube_calendar_ui(settings) } }; - // add the given list of participants var add_attendees = function(names, params) { diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index abe31cff..d8fcb38b 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -115,6 +115,7 @@ body.calendarmain #mainscreen { width: 6px; top: 40px !important; bottom: 0; + height: auto; background: url(images/toggle.gif) -1px 48% no-repeat transparent; } @@ -1245,44 +1246,44 @@ td.topalign { background: url(images/loading_blue.gif) center no-repeat; } -#schedule-freebusy-times td.unknown, +#schedule-freebusy-times td div.unknown, .availability img.availabilityicon.unknown { background: #ddd; } -#schedule-freebusy-times td.free, +#schedule-freebusy-times td div.free, .availability img.availabilityicon.free { background: #abd640; } -#schedule-freebusy-times td.busy, +#schedule-freebusy-times td div.busy, .availability img.availabilityicon.busy { background: #e26569; } -#schedule-freebusy-times td.tentative, +#schedule-freebusy-times td div.tentative, .availability img.availabilityicon.tentative { background: #8383fc; } -#schedule-freebusy-times td.out-of-office, +#schedule-freebusy-times td div.out-of-office, .availability img.availabilityicon.out-of-office { background: #fbaa68; } -#schedule-freebusy-times td.all-busy, -#schedule-freebusy-times td.all-tentative, -#schedule-freebusy-times td.all-out-of-office { +#schedule-freebusy-times td div.all-busy, +#schedule-freebusy-times td div.all-tentative, +#schedule-freebusy-times td div.all-out-of-office { background-image: url(images/freebusy-colors.png); background-position: top right; background-repeat: no-repeat; } -#schedule-freebusy-times td.all-tentative { +#schedule-freebusy-times td div.all-tentative { background-position: right -40px; } -#schedule-freebusy-times td.all-out-of-office { +#schedule-freebusy-times td div.all-out-of-office { background-position: right -80px; } @@ -1409,7 +1410,8 @@ td.topalign { .attendees-list .spacer, #schedule-freebusy-times tr.spacer td { background: 0; - font-size: 50%; + padding: 0; + height: 10px; } #schedule-freebusy-times { @@ -1422,6 +1424,15 @@ td.topalign { border: 1px solid #ccc; } +#schedule-freebusy-times tbody td { + padding: 0; + height: 20px; +} + +#schedule-freebusy-times tbody td div { + height: 100%; +} + #attendees-freebusy-table div.timesheader, #schedule-freebusy-times tr.times td { min-width: 30px; @@ -1439,6 +1450,10 @@ td.topalign { cursor: pointer; } +#schedule-freebusy-times #fbrowall td { + border-bottom: none; +} + #schedule-event-time { position: absolute; border: 2px solid #333;