Fixed smart event segmentation in list view; added table view (derived from list view) as alternative

This commit is contained in:
Thomas Bruederli 2011-06-11 13:31:24 -06:00
parent 5f7e3b6029
commit b45566de9e
7 changed files with 405 additions and 72 deletions

View file

@ -739,7 +739,7 @@ function rcube_calendar(settings)
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaDay,agendaWeek,month,list'
right: 'agendaDay,agendaWeek,month,list,table'
},
aspectRatio: 1,
ignoreTimezone: false, // will translate event dates to the client's timezone
@ -754,20 +754,25 @@ function rcube_calendar(settings)
slotMinutes : 60/settings['timeslots'],
timeFormat: {
'': settings['time_format'],
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
},
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'] // Monday 9/7
day: 'dddd ' + settings['date_short'], // Monday 9/7
list: settings['date_agena'],
table: settings['date_agena']
},
titleFormat: {
month: 'MMMM yyyy',
week: settings['date_long'].replace(/ yyyy/, '[ yyyy]') + "{ '—' " + settings['date_long'] + "}",
day: 'dddd ' + settings['date_long'],
list: settings['date_long']
list: settings['date_long'],
table: settings['date_long']
},
smartSections: true,
defaultView: settings['default_view'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
@ -776,7 +781,7 @@ function rcube_calendar(settings)
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
list: rcmail.gettext('agenda', 'calendar'),
basicDay: 'basic'
table: rcmail.gettext('table', 'calendar')
},
selectable: true,
selectHelper: true,
@ -785,7 +790,7 @@ function rcube_calendar(settings)
},
// event rendering
eventRender: function(event, element, view) {
if (view.name != 'list')
if (view.name != 'list' && view.name != 'table')
element.attr('title', event.title);
if (view.name == 'month') {
/* attempt to limit the number of events displayed
@ -940,7 +945,7 @@ function rcube_calendar(settings)
var shift_enddate = function(dateText) {
var newstart = parse_datetime('0', dateText);
var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
$('#edit-enddate').val($.fullCalendar.formatDate(newend, cal.settings['date_format']));
$('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
};
// init event dialog

View file

@ -370,6 +370,7 @@ class calendar extends rcube_plugin
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
// TODO: keep view and date selection
if ($success && $reload)
$this->rc->output->redirect('');
}
@ -478,6 +479,7 @@ class calendar extends rcube_plugin
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', "yyyy/MM/dd");
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', "M/d");
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', "M d yyyy");
$settings['date_agena'] = (string)$this->rc->config->get('calendar_date_agenda', "ddd M d");
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', "HH:mm");
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', 2);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', 1);

View file

@ -40,6 +40,9 @@ $rcmail_config['calendar_date_short'] = 'M-d';
// long date format (used for calendar title)
$rcmail_config['calendar_date_long'] = 'MMM d yyyy';
// date format used for agenda view
$rcmail_config['calendar_date_agenda'] = 'ddd MM-dd';
// timeslots per hour (1, 2, 3, 4, 6)
$rcmail_config['calendar_timeslots'] = 2;

View file

@ -1,5 +1,5 @@
--- js/fullcalendar.js.orig 2011-06-04 13:45:44.000000000 -0600
+++ js/fullcalendar.js 2011-06-08 14:30:33.000000000 -0600
+++ js/fullcalendar.js 2011-06-10 09:27:50.000000000 -0600
@@ -47,12 +47,14 @@
titleFormat: {
month: 'MMMM yyyy',
@ -34,7 +34,7 @@
+ thisWeek: 'This week',
+ nextWeek: 'Next week',
+ thisMonth: 'This month',
+ nextMonth: 'Next Month',
+ nextMonth: 'Next month',
+ future: 'Future events'
},
@ -59,19 +59,19 @@
rangeStart = start;
rangeEnd = end;
- cache = [];
+ cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, source); }) : [];
+ cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, src); }) : [];
var fetchID = ++currentFetchID;
var len = sources.length;
- pendingSourceCnt = len;
+ pendingSourceCnt = typeof src == 'undefined' ? len : 1;
for (var i=0; i<len; i++) {
- fetchEventSource(sources[i], fetchID);
+ if (typeof src == 'undefined' || src == sources[i])
+ if (typeof src == 'undefined' || isSourcesEqual(sources[i], src))
+ fetchEventSource(sources[i], fetchID);
}
}
@@ -5205,4 +5221,307 @@
@@ -5205,4 +5221,309 @@
}
@ -269,6 +269,8 @@
+
+ lazySegBind(segContainer, seg, bindSeg);
+ }
+
+ markFirstLast(getListContainer());
+ }
+
+ function bindSeg(event, eventElement, seg) {
@ -364,7 +366,7 @@
+ }
+
+ function setHeight(height, dateChanged) {
+ body.css('height', height+'px').css('overflow', 'auto');
+ body.css('height', (height-1)+'px').css('overflow', 'auto');
+ }
+
+ function setWidth(width) {

View file

@ -48,13 +48,15 @@ var defaults = {
month: 'MMMM yyyy',
week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
day: 'dddd, MMM d, yyyy',
list: 'MMM d, yyyy'
list: 'MMM d, yyyy',
table: 'MMM d, yyyy'
},
columnFormat: {
month: 'ddd',
week: 'ddd M/d',
day: 'dddd M/d',
list: 'dddd, yyyy'
list: 'dddd, MMM d, yyyy',
table: 'dddd, MMM d, yyyy'
},
timeFormat: { // for event elements
'': 'h(:mm)t' // default
@ -76,7 +78,8 @@ var defaults = {
month: 'month',
week: 'week',
day: 'day',
list: 'list'
list: 'list',
table: 'table'
},
listTexts: {
from: 'from',
@ -91,6 +94,9 @@ var defaults = {
future: 'Future events'
},
// list options
smartSections: false,
// jquery-ui theming
theme: false,
buttonIcons: {
@ -5230,8 +5236,11 @@ function ListEventRenderer() {
// exports
t.renderEvents = renderEvents;
t.renderEventTime = renderEventTime;
t.compileDaySegs = compileSegs; // for DayEventRenderer
t.clearEvents = clearEvents;
t.lazySegBind = lazySegBind;
t.sortCmp = sortCmp;
// imports
DayEventRenderer.call(t);
@ -5265,9 +5274,12 @@ function ListEventRenderer() {
function compileSegs(events) {
var segs = [];
var colFormat = opt('columnFormat', 'day');
var event, i, dd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
var colFormat = opt('titleFormat', 'day');
var firstDay = opt('firstDay');
var smartSegs = opt('smartSections');
var event, i, dd, wd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
var today = clearTime(new Date());
var weekstart = addDays(cloneDate(today), -((today.getDay() - firstDay + 7) % 7));
for (i=0; i < events.length; i++) {
event = events[i];
@ -5280,40 +5292,33 @@ function ListEventRenderer() {
// create smart sections such as today, tomorrow, this week, next week, next month, ect.
segDate = cloneDate(event.start < t.start && event.end > t.start ? t.start : event.start, true);
dd = dayDiff(segDate, today);
wd = Math.floor(dayDiff(segDate, weekstart) / 7);
md = segDate.getMonth() + ((segDate.getYear() - today.getYear()) * 12) - today.getMonth();
// past events
if (dd < 0) {
// build section title
if (!smartSegs) {
segHash = formatDate(segDate, colFormat);
} else if (dd < 0) {
segHash = opt('listTexts', 'past');
}
// today
else if (dd == 0) {
} else if (dd == 0) {
segHash = opt('listTexts', 'today');
}
else if (dd == 1) {
} else if (dd == 1) {
segHash = opt('listTexts', 'tomorrow');
}
// this week
else if (dd < 7) {
} else if (wd == 0) {
segHash = opt('listTexts', 'thisWeek');
}
// next week
else if (dd >= 7 && dd < 14 && md == 0) {
} else if (wd == 1) {
segHash = opt('listTexts', 'nextWeek');
}
else if (md == 0) {
} else if (md == 0) {
segHash = opt('listTexts', 'thisMonth');
}
else if (md == 1) {
} else if (md == 1) {
segHash = opt('listTexts', 'nextMonth');
}
else {
} else {
segHash = formatDate(segDate, colFormat);
}
// start new segment
if (segHash != curSegHash) {
segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd };
segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd, weekdiff: wd, monthdiff: md };
curSegHash = segHash;
}
@ -5329,11 +5334,9 @@ function ListEventRenderer() {
function renderSegs(segs, modifiedEventId) {
var tm = opt('theme') ? 'ui' : 'fc';
var timeFormat = opt('timeFormat');
var dateFormat = opt('titleFormat');
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var i, j, seg, event, duration, s, skinCss, skinCssAttr, classes, time, segHeader, segContainer, eventElements;
var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segHeader, segContainer, eventElements;
for (j=0; j < segs.length; j++) {
seg = segs[j];
@ -5344,7 +5347,7 @@ function ListEventRenderer() {
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
times = renderEventTime(event, seg);
skinCss = getSkinCss(event, opt);
skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
classes = ['fc-event', 'fc-event-skin', 'fc-event-vert', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'];
@ -5352,31 +5355,13 @@ function ListEventRenderer() {
classes = classes.concat(event.source.className);
}
// event time/date range to display
times = [];
duration = event.end.getTime() - event.start.getTime();
if (event.start < seg.start) {
times.push(opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat));
} else if (duration > DAY_MS) {
times.push(formatDates(event.start, event.end, dateFormat + '[ - ' + dateFormat + ']'));
} else if (seg.daydiff > 1 && seg.daydiff < 7) {
times.push(formatDate(event.start, 'ddd'));
} else if (seg.daydiff > 1 || seg.daydiff < 0) {
times.push(formatDate(event.start, dateFormat));
}
if (!times.length && event.allDay) {
times.push(opt('allDayText'));
} else if (duration < DAY_MS && !event.allDay) {
times.push(formatDates(event.start, event.end, timeFormat))
}
s +=
"<div class='" + classes.join(' ') + "'" + skinCssAttr + ">" +
"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-time'>" +
htmlEscape(times.join(' ')) +
(times[0] ? '<span class="fc-col-date">' + times[0] + '</span> ' : '') +
(times[1] ? '<span class="fc-col-time">' + times[1] + '</span>' : '') +
"</div>" +
"</div>" +
"<div class='fc-event-content'>" +
@ -5405,7 +5390,7 @@ function ListEventRenderer() {
eventElement = $(triggerRes).appendTo(segContainer);
}
if (event._id === modifiedEventId) {
bindSeg(event, eventElement, seg);
eventElementHandlers(event, eventElement, seg);
} else {
eventElement[0]._fci = i; // for lazySegBind
}
@ -5413,16 +5398,44 @@ function ListEventRenderer() {
}
}
lazySegBind(segContainer, seg, bindSeg);
lazySegBind(segContainer, seg, eventElementHandlers);
}
markFirstLast(getListContainer());
}
function bindSeg(event, eventElement, seg) {
eventElementHandlers(event, eventElement);
// event time/date range to display
function renderEventTime(event, seg) {
var timeFormat = opt('timeFormat');
var dateFormat = opt('columnFormat');
var duration = event.end.getTime() - event.start.getTime();
var datestr = '', timestr = '';
if (!opt('smartSections')) {
// no date display if grouped by day
} else if (event.start < seg.start) {
datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
} else if (duration > DAY_MS) {
datestr = formatDates(event.start, event.end, dateFormat + '[ - ' + dateFormat + ']');
} else if (seg.daydiff == 0) {
datestr = opt('listTexts', 'today');
} else if (seg.daydiff == 1) {
datestr = opt('listTexts', 'tomorrow');
} else if (seg.weekdiff == 0 || seg.weekdiff == 1) {
datestr = formatDate(event.start, 'dddd');
} else if (seg.daydiff > 1 || seg.daydiff < 0) {
datestr = formatDate(event.start, dateFormat);
}
if (!datestr && event.allDay) {
timestr = opt('allDayText');
} else if (duration < DAY_MS && !event.allDay) {
timestr = formatDates(event.start, event.end, timeFormat);
}
return [datestr, timestr];
}
function lazySegBind(container, seg, bindHandlers) {
container.unbind('mouseover').mouseover(function(ev) {
var parent = ev.target, e = parent, i, event;
@ -5526,4 +5539,226 @@ function ListView(element, calendar) {
}
/* Additional view: table (by bruederli@kolabsys.com)
---------------------------------------------------------------------------------*/
function TableEventRenderer() {
var t = this;
// imports
ListEventRenderer.call(t);
var opt = t.opt;
var sortCmp = t.sortCmp;
var trigger = t.trigger;
var compileSegs = t.compileDaySegs;
var reportEvents = t.reportEvents;
var reportEventClear = t.reportEventClear;
var reportEventElement = t.reportEventElement;
var eventElementHandlers = t.eventElementHandlers;
var renderEventTime = t.renderEventTime;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var getListContainer = t.getDaySegmentContainer;
var lazySegBind = t.lazySegBind;
var calendar = t.calendar;
var formatDate = calendar.formatDate;
var formatDates = calendar.formatDates;
// exports
t.renderEvents = renderEvents;
t.clearEvents = clearEvents;
/* Rendering
--------------------------------------------------------------------*/
function clearEvents() {
reportEventClear();
getListContainer().children('tbody').remove();
}
function renderEvents(events, modifiedEventId) {
events.sort(sortCmp);
reportEvents(events);
renderSegs(compileSegs(events), modifiedEventId);
}
function renderSegs(segs, modifiedEventId) {
var tm = opt('theme') ? 'ui' : 'fc';
var table = getListContainer();
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segHeader, segContainer, eventElements;
for (j=0; j < segs.length; j++) {
seg = segs[j];
segHeader = $('<tbody class="fc-list-header"><tr><td class="fc-list-header ' + headerClass + '" colspan="5">' + htmlEscape(seg.title) + '</td></tr></tbody>').appendTo(table);
segContainer = $('<tbody>').addClass('fc-list-section ' + contentClass).appendTo(table);
s = '';
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
times = renderEventTime(event, seg);
skinCss = getSkinCss(event, opt);
skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
skinClasses = ['fc-event-skin', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'];
if (event.source && event.source.className) {
skinClasses = skinClasses.concat(event.source.className);
}
rowClasses = ['fc-event', 'fc-event-row', 'fc-'+dayIDs[event.start.getDay()]];
if (seg.daydiff == 0) {
rowClasses.push('fc-today');
}
s +=
"<tr class='" + rowClasses.join(' ') + "'>" +
"<td class='fc-event-handle'>" +
"<div class='" + skinClasses.join(' ') + "'" + skinCssAttr + ">" +
"<span class='fc-event-inner'></span>" +
"</div></td>" +
"<td class='fc-event-date'>" +
htmlEscape(times[0]) +
"</td>" +
"<td class='fc-event-time'>" +
htmlEscape(times[1]) +
"</td>" +
"<td class='fc-event-title'>" +
htmlEscape(event.title) +
"</td>" +
"<td class='fc-event-location'>" +
htmlEscape(event.location) +
"</td>" +
"</tr>";
}
segContainer[0].innerHTML = s;
eventElements = segContainer.children();
// retrieve elements, run through eventRender callback, bind event handlers
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
eventElement = $(eventElements[i]); // faster than eq()
triggerRes = trigger('eventRender', event, event, eventElement);
if (triggerRes === false) {
eventElement.remove();
} else {
if (triggerRes && triggerRes !== true) {
eventElement.remove();
eventElement = $(triggerRes).appendTo(segContainer);
}
if (event._id === modifiedEventId) {
eventElementHandlers(event, eventElement, seg);
} else {
eventElement[0]._fci = i; // for lazySegBind
}
reportEventElement(event, eventElement);
}
}
lazySegBind(segContainer, seg, eventElementHandlers);
markFirstLast(segContainer);
}
//markFirstLast(table);
}
}
fcViews.table = TableView;
function TableView(element, calendar) {
var t = this;
// exports
t.render = render;
t.select = dummy;
t.unselect = dummy;
t.getDaySegmentContainer = function(){ return table; };
// imports
View.call(t, element, calendar, 'table');
TableEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var clearEvents = t.clearEvents;
var reportEventClear = t.reportEventClear;
var formatDates = calendar.formatDates;
var formatDate = calendar.formatDate;
// overrides
t.setWidth = setWidth;
t.setHeight = setHeight;
// locals
var div;
var table;
var firstDay;
var nwe;
var tm;
var colFormat;
function render(date, delta) {
if (delta) {
addDays(date, delta);
if (!opt('weekends')) {
skipWeekend(date, delta < 0 ? -1 : 1);
}
}
t.title = opt('listTexts', 'from') + ' ' + formatDate(date, opt('titleFormat'));
t.start = t.visStart = cloneDate(date, true);
t.end = addDays(cloneDate(t.start), 1);
t.visEnd = addMonths(cloneDate(t.start), 1); // show events one month ahead. Enough?
updateOptions();
if (!table) {
buildSkeleton();
} else {
clearEvents();
}
}
function updateOptions() {
firstDay = opt('firstDay');
nwe = opt('weekends') ? 0 : 1;
tm = opt('theme') ? 'ui' : 'fc';
colFormat = opt('columnFormat');
}
function buildSkeleton() {
var s =
"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
"<colgroup>" +
"<col class='fc-event-handle' />" +
"<col class='fc-event-date' />" +
"<col class='fc-event-time' />" +
"<col class='fc-event-title' />" +
"<col class='fc-event-location' />" +
"</colgroup>" +
"</table>";
div = $('<div>').addClass('fc-list-content').appendTo(element);
table = $(s).appendTo(div);
}
function setHeight(height, dateChanged) {
div.css('height', (height-1)+'px').css('overflow', 'auto');
}
function setWidth(width) {
// nothing to be done here
}
function dummy() {
// Stub.
}
}
})(jQuery);

View file

@ -461,16 +461,21 @@ a.alarm-action-snooze:after {
.fc-event-hori .fc-event-time {
white-space: nowrap;
font-weight: normal;
font-weight: normal !important;
font-size: 10px;
padding-right: 0.6em;
}
.fc-grid .fc-event-time {
font-weight: normal !important;
padding-right: 0.3em;
}
.fc-event-cateories {
font-style:italic;
}
.fc-event-location {
div.fc-event-location {
font-size: 90%;
}
@ -504,6 +509,27 @@ a.alarm-action-snooze:after {
cursor: pointer;
}
.fc-view-list div.fc-list-header,
.fc-view-table td.fc-list-header {
padding: 3px;
background: #dddddd;
background-image: -moz-linear-gradient(center top, #f4f4f4, #d2d2d2);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.00, #f4f4f4), color-stop(1.00, #d2d2d2));
filter: progid:DXImageTransform.Microsoft.gradient(enabled='true', startColorstr=#f4f4f4, endColorstr=#d2d2d2, GradientType=1);
font-weight: bold;
color: #333;
}
.fc-view-list .fc-event-skin .fc-event-content {
background: #F6F6F6;
padding: 2px;
}
.fc-view-list .fc-event-skin .fc-event-title,
.fc-view-list .fc-event-skin .fc-event-location {
color: #333;
}
/* Settings section */
fieldset #calendarcategories div {

View file

@ -619,18 +619,28 @@ table.fc-border-separate {
/* List view (by bruederli@kolabsys.com)
------------------------------------------------------------------------*/
.fc-view-list {
.fc-view-list,
.fc-view-table {
border: 1px solid #ccc;
width: 99%;
}
.fc-view-list .fc-list-header {
.fc-view-list .fc-list-header,
.fc-view-table td.fc-list-header {
border-width: 0;
border-bottom-width: 1px;
padding: 2px;
padding: 3px;
text-align: center;
}
.fc-view-table td.fc-list-header {
_border-top-width: 1px;
}
.fc-view-table .fc-first td.fc-list-header {
border-top-width: 0;
}
.fc-list-section {
padding: 4px 2px;
border-width: 0;
@ -645,3 +655,53 @@ table.fc-border-separate {
position: relative;
margin: 1px 2px 3px 2px;
}
.fc-view-table tr.fc-event td {
padding: 2px;
border-bottom: 1px solid #ccc;
}
.fc-view-table tr.fc-event td.fc-event-handle {
padding: 3px 8px 3px 3px;
}
.fc-view-table .fc-event-handle .fc-event-skin {
border-radius: 2px;
-moz-border-radius: 2px;
}
.fc-view-table .fc-event-handle .fc-event-inner {
display: block;
width: 8px;
height: 10px;
border-radius: 2px;
-moz-border-radius: 2px;
}
.fc-view-table table {
table-layout: fixed;
width: 100%;
}
.fc-view-table col.fc-event-handle {
width: 18px;
}
.fc-view-table col.fc-event-date {
width: 7em;
}
.fc-view-table col.fc-event-time {
width: 8em;
}
.fc-view-table col.fc-event-location {
width: 20%;
}
.fc-view-table td.fc-event-date,
.fc-view-table td.fc-event-time {
white-space: nowrap;
padding-right: 1em;
}