diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index c51eb222..9f02b7c1 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -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 diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 9ffedf37..81a5184b 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -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); diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index 8abc924a..aa580071 100644 --- a/plugins/calendar/config.inc.php.dist +++ b/plugins/calendar/config.inc.php.dist @@ -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; diff --git a/plugins/calendar/lib/fullcalendar-rc.patch b/plugins/calendar/lib/fullcalendar-rc.patch index 13cba49d..a886f25d 100644 --- a/plugins/calendar/lib/fullcalendar-rc.patch +++ b/plugins/calendar/lib/fullcalendar-rc.patch @@ -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 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 += "
" + "
" + "
" + "
" + - htmlEscape(times.join(' ')) + + (times[0] ? '' + times[0] + ' ' : '') + + (times[1] ? '' + times[1] + '' : '') + "
" + "
" + "
" + @@ -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 = $('' + htmlEscape(seg.title) + '').appendTo(table); + segContainer = $('').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 += + "" + + "" + + "
" + + "" + + "
" + + "" + + htmlEscape(times[0]) + + "" + + "" + + htmlEscape(times[1]) + + "" + + "" + + htmlEscape(event.title) + + "" + + "" + + htmlEscape(event.location) + + "" + + ""; + } + + 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 = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
"; + 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); \ No newline at end of file diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 7705f0fd..ec53c8ea 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -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 { diff --git a/plugins/calendar/skins/default/fullcalendar.css b/plugins/calendar/skins/default/fullcalendar.css index be09068d..e14cad45 100644 --- a/plugins/calendar/skins/default/fullcalendar.css +++ b/plugins/calendar/skins/default/fullcalendar.css @@ -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; +} +