diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -2185,6 +2185,7 @@ // 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 @@ $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 @@ // 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 @@ } } } - - $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 @@ '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 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -1238,7 +1238,7 @@ 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 @@ // 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 @@ 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 @@ // 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 @@ } // add line for all/required attendees - times_html += ' '; + times_html += ''; times_html += '' + slots_row + ''; var table = $('#schedule-freebusy-times'); @@ -1429,7 +1431,7 @@ 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 @@ 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; - + slotnum = freebusy_ui.interval > 60 ? 1 : (60 / freebusy_ui.interval), + cells = table.children('thead').find('td'), + cell_width = cells.first().get(0).offsetWidth, + slotend; + // 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; + 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; } - - slotstart = 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 @@ } 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 @@ 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 @@ 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 @@ } // 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 @@ 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'); - - // 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]; + 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); + + 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ } }; - // update event properties and attendees availability if event times have changed var event_times_changed = function() { @@ -1821,7 +1850,6 @@ } }; - // 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 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -115,6 +115,7 @@ width: 6px; top: 40px !important; bottom: 0; + height: auto; background: url(images/toggle.gif) -1px 48% no-repeat transparent; } @@ -1245,44 +1246,44 @@ 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 @@ .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 @@ 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 @@ cursor: pointer; } +#schedule-freebusy-times #fbrowall td { + border-bottom: none; +} + #schedule-event-time { position: absolute; border: 2px solid #333;