diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 343b3e5d..6c23741b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -194,6 +194,7 @@ class calendar extends rcube_plugin
}
$this->add_hook('messages_list', array($this, 'mail_messages_list'));
+ $this->add_hook('message_compose', array($this, 'mail_message_compose'));
}
else if ($args['task'] == 'addressbook') {
if ($this->rc->config->get('calendar_contact_birthdays')) {
@@ -973,6 +974,95 @@ class calendar extends rcube_plugin
$success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
}
break;
+
+ case "changelog":
+ $data = $this->driver->get_event_changelog($event);
+ if (is_array($data) && !empty($data)) {
+ $lib = $this->lib;
+ array_walk($data, function($change) use ($lib) {
+ if ($change['date']) {
+ $dt = $lib->adjust_timezone($change['date']);
+ if ($dt instanceof DateTime)
+ $change['date'] = $dt->format('c');
+ }
+ });
+ $this->rc->output->command('plugin.render_event_changelog', $data);
+ }
+ else {
+ $this->rc->output->command('plugin.render_event_changelog', false);
+ $this->rc->output->command('display_message', $this->gettext('eventchangelognotavailable'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "diff":
+ $data = $this->driver->get_event_diff($event, $event['rev']);
+ if (is_array($data)) {
+ // convert some properties, similar to self::_client_event()
+ $lib = $this->lib;
+ array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
+ // convert date cols
+ foreach (array('start','end','created','changed') as $col) {
+ if ($change['property'] == $col) {
+ $change['old'] = $this->lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
+ $change['new'] = $this->lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
+ }
+ }
+ // create textual representation for alarms and recurrence
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']))
+ $change['old_'] = libcalendaring::alarm_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'recurrence') {
+ if (is_array($change['old']))
+ $change['old_'] = $lib->recurrence_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'attachments') {
+ if (is_array($change['old']))
+ $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
+ if (is_array($change['new']))
+ $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
+ }
+ // compute a nice diff of description texts
+ if ($change['property'] == 'description') {
+ $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+ }
+ });
+ $this->rc->output->command('plugin.event_show_diff', $data);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('eventdiffnotavailable'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "show":
+ if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
+ $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('eventnotfound'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "restore":
+ if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
+
+ }
+ else {
+ $this->rc->output->command('display_message', 'Not implemented yet', 'error');
+ $got_msg = true;
+ }
+ $reload = false;
+ break;
}
// show confirmation/error message
@@ -1271,22 +1361,33 @@ class calendar extends rcube_plugin
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
+ $event_id = get_input_value('id', RCUBE_INPUT_GET);
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
- $calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
+ $calid = $filename = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
+ $events = array();
if ($calendars[$calid]) {
- $calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
- $calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
- if (empty($calname)) $calname = $calid;
- $events = $this->driver->load_events($start, $end, null, $calid, 0);
+ $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
+ $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
+ if (!empty($event_id)) {
+ if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id))) {
+ $events = array($event);
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+ }
+ }
+ else {
+ $events = $this->driver->load_events($start, $end, null, $calid, 0);
+ if (empty($filename))
+ $filename = $calid;
+ }
}
- else
- $events = array();
header("Content-Type: text/calendar");
- header("Content-Disposition: inline; filename=".$calname.'.ics');
+ header("Content-Disposition: inline; filename=".$filename.'.ics');
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
@@ -2672,6 +2773,34 @@ class calendar extends rcube_plugin
$this->rc->output->send();
}
+ /**
+ * Handler for the 'message_compose' plugin hook. This will check for
+ * a compose parameter 'calendar_event' and create an attachment with the
+ * referenced event in iCal format
+ */
+ public function mail_message_compose($args)
+ {
+ // set the submitted event ID as attachment
+ if (!empty($args['param']['calendar_event'])) {
+ $this->load_driver();
+
+ list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
+ if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+
+ // save ics to a temp file and register as attachment
+ $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
+ file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
+
+ $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar');
+ $args['param']['subject'] = $event['title'];
+ }
+ }
+
+ return $args;
+ }
/**
* Checks if specified message part is a vcalendar data
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 8b59ef3c..8d4433d4 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -252,6 +252,11 @@ function rcube_calendar_ui(settings)
return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14));
}
+ var format_datetime = function(date, mode, voice)
+ {
+ return me.format_datetime(date, mode, voice);
+ }
+
var render_link = function(url)
{
var islink = false, href = url;
@@ -304,11 +309,13 @@ function rcube_calendar_ui(settings)
var load_attachment = function(event, att)
{
- var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
+ var query = { _id: att.id, _event: event.recurrence_id || event.id, _cal:event.calendar, _frame: 1 };
+ if (event.rev)
+ query._rev = event.rev;
// open attachment in frame if it's of a supported mimetype
if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
- if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
+ if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
return;
}
}
@@ -378,15 +385,23 @@ function rcube_calendar_ui(settings)
};
// event details dialog (show only)
- var event_show_dialog = function(event, ev)
+ var event_show_dialog = function(event, ev, temp)
{
- var $dialog = $("#eventshow").attr('class', 'uidialog');
+ var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
- me.selected_event = event;
+
+ if (!temp)
+ me.selected_event = event;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
+ // convert start/end dates if not done yet by fullcalendar
+ if (typeof event.start == 'string')
+ event.start = parseISO8601(event.start);
+ if (typeof event.end == 'string')
+ event.end = parseISO8601(event.end);
+
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@@ -430,6 +445,13 @@ function rcube_calendar_ui(settings)
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+event.sensitivity);
}
+ if (event.created || event.changed) {
+ var created = parseISO8601(event.created),
+ changed = parseISO8601(event.changed)
+ $('#event-created-changed .event-created').html(Q(created ? format_datetime(created) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed .event-changed').html(Q(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed').show()
+ }
// create attachments list
if ($.isArray(event.attachments)) {
@@ -451,14 +473,10 @@ function rcube_calendar_ui(settings)
return (j - k);
});
- var data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '',overflow = '';
+ var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
- dispname = Q(data.name || data.email);
- tooltip = '';
if (data.email) {
- tooltip = data.email;
- dispname = '' + dispname + '';
if (data.role == 'ORGANIZER')
organizer = true;
else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
@@ -467,18 +485,14 @@ function rcube_calendar_ui(settings)
rsvp = mystatus;
}
}
-
- if (data['delegated-to'])
- tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
- else if (data['delegated-from'])
- tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
-
- line = '' + dispname + ' ';
+
+ line = event_attendee_html(data);
+
if (morelink)
overflow += line;
else
html += line;
-
+
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
morelink = $('').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1));
@@ -522,7 +536,7 @@ function rcube_calendar_ui(settings)
}
var buttons = {};
- if (calendar.editable && event.editable !== false) {
+ if (!temp && calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
@@ -551,6 +565,13 @@ function rcube_calendar_ui(settings)
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ rcmail.command('menu-close','eventoptionsmenu')
+ },
+ dragStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu')
+ },
+ resizeStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu')
},
buttons: buttons,
minWidth: 320,
@@ -566,15 +587,38 @@ function rcube_calendar_ui(settings)
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
-/*
+
// add link for "more options" drop-down
- $('')
- .attr('href', '#')
- .html('More Options')
- .addClass('dropdown-link')
- .click(function(){ return false; })
- .insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
-*/
+ if (!temp) {
+ $('')
+ .attr('href', '#')
+ .html(rcmail.gettext('eventoptions','calendar'))
+ .addClass('dropdown-link')
+ .click(function(e) {
+ return rcmail.command('menu-open','eventoptionsmenu', this, e)
+ })
+ .appendTo($dialog.parent().find('.ui-dialog-buttonset'));
+ }
+
+ rcmail.enable_command('event-history', calendar.history)
+ };
+
+ // render HTML code for displaying an attendee record
+ var event_attendee_html = function(data)
+ {
+ var dispname = Q(data.name || data.email), tooltip = '';
+
+ if (data.email) {
+ tooltip = data.email;
+ dispname = '' + dispname + '';
+ }
+
+ if (data['delegated-to'])
+ tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
+ else if (data['delegated-from'])
+ tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
+
+ return '' + dispname + ' ';
};
// event handler for clicks on an attendee link
@@ -586,7 +630,7 @@ function rcube_calendar_ui(settings)
event_resources_dialog(mailto);
}
else {
- rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
+ rcmail.command('compose', mailto, e ? e.target : null, e);
}
return false;
};
@@ -876,6 +920,291 @@ function rcube_calendar_ui(settings)
window.setTimeout(load_attachments_tab, exec_deferred);
};
+ // show event changelog in a dialog
+ var event_history_dialog = function(event)
+ {
+ if (!event.id)
+ return false
+
+ // render dialog
+ $dialog = $('#eventhistory');
+
+ // close show dialog first
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // hide and reset changelog table
+ $('#event-changelog-table').children('tbody')
+ .html('
'+ rcmail.gettext('loading') +' |
');
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('eventchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 450,
+ width: 650,
+ height: 350,
+ minHeight: 200,
+ })
+ .data('event', event)
+ .show().children('.compare-button').hide();
+
+ // set dialog size according to content
+ // me.dialog_resize($dialog.get(0), $dialog.height(), 650);
+
+ // fetch changelog data
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
+
+ // initialize event handlers for history dialog UI elements
+ if (!$dialog.data('initialized')) {
+ // compare button
+ $dialog.find('.compare-button input').click(function(e) {
+ var rev1 = $('#event-changelog-table input.diff-rev1:checked').val(),
+ rev2 = $('#event-changelog-table input.diff-rev2:checked').val(),
+ event = $('#eventhistory').data('event');
+
+ if (rev1 && rev2 && rev1 != rev2) {
+ // swap revisions if the user got it wrong
+ if (rev1 > rev2) {
+ var tmp = rev2;
+ rev2 = rev1;
+ rev1 = tmp;
+ }
+
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev: rev1+':'+rev2 } }, me.loading_lock);
+ }
+ else {
+ alert('Invalid selection!')
+ }
+ });
+
+ // delegate handlers for list actions
+ $('#event-changelog-table tbody').on('click', 'td.actions a', function(e) {
+ var link = $(this),
+ action = link.hasClass('restore') ? 'restore' : 'show',
+ event = $('#eventhistory').data('event'),
+ rev = link.attr('data-rev');
+
+ // ignore clicks on first row (current revision)
+ if (link.closest('tr').hasClass('first')) {
+ return false;
+ }
+
+ // let the user confirm the restore action
+ if (action == 'restore' && !confirm(rcmail.gettext('eventrestoreconfirm','calendar').replace('$rev', rev))) {
+ return false;
+ }
+
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
+ return false;
+ });
+
+ $dialog.data('initialized', true);
+ }
+ };
+
+ // callback from server with changelog data
+ var render_event_changelog = function(data)
+ {
+ var $dialog = $('#eventhistory');
+
+ if (data === false || !data.length) {
+ $dialog.dialog('close');
+ return
+ }
+
+ var i, change, accessible, op_append, first = data.length -1, last = 0,
+ op_labels = { APPEND: 'actionappend', MOVE: 'actionmove', DELETE: 'actiondelete' },
+ actions = ' ' +
+ '',
+ tbody = $('#event-changelog-table tbody').html('');
+
+ for (i=first; i >= 0; i--) {
+ change = data[i];
+ accessible = change.date && change.user;
+
+ if (change.op == 'MOVE' && change.folder) {
+ op_append = ' ⇢ ' + change.folder;
+ }
+ else {
+ op_append = '';
+ }
+
+ $('')
+ .append('' + (accessible && change.op != 'DELETE' ?
+ ' '+
+ ' | '
+ : ''))
+ .append('' + Q(change.rev) + ' | ')
+ .append('' + Q(change.date ? format_datetime(parseISO8601(change.date)) : '') + ' | ')
+ .append('' + Q(change.user || 'undisclosed') + ' | ')
+ .append('' + Q(rcmail.gettext(op_labels[change.op] || '', 'calendar') + (op_append ? ' ...' : '')) + ' | ')
+ .append('' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + ' | ')
+ .appendTo(tbody);
+ }
+
+ $('#eventhistory .compare-button').fadeIn(200);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+ };
+
+ // callback from server with event diff data
+ var event_show_diff = function(data)
+ {
+ var event = me.selected_event,
+ $dialog = $("#eventdiff");
+
+ $dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
+ $dialog.find('div.event-section.clone, div.event-line.clone').remove();
+
+ // always show event title and date
+ $('.event-title', $dialog).html(Q(event.title)).removeClass('event-text-old').show();
+ $('.event-date', $dialog).html(Q(me.event_date_text(event))).show();
+
+ // show each property change
+ $.each(data.changes, function(i,change) {
+ var prop = change.property, r2, html = false,
+ row = $('div.event-' + prop, $dialog).first();
+
+ // special case: title
+ if (prop == 'title') {
+ $('.event-title', $dialog).addClass('event-text-old').html(Q(change.old || '--'));
+ $('.event-title-new', $dialog).html(Q(change.new || '--')).show();
+ }
+
+ // no display container for this property
+ if (!row.length) {
+ return true;
+ }
+
+ // clone row if already exists
+ if (row.data('set')) {
+ r2 = row.clone().addClass('clone').insertAfter(row);
+ row = r2;
+ }
+
+ // format dates
+ if (['start','end','changed'].indexOf(prop) >= 0) {
+ if (change.old) change.old_ = me.format_datetime(parseISO8601(change.old));
+ if (change.new) change.new_ = me.format_datetime(parseISO8601(change.new));
+ }
+ // render description text
+ else if (prop == 'description') {
+ // TODO: show real text diff
+ if (!change.diff_ && change.old) change.old_ = text2html(change.old);
+ if (!change.diff_ && change.new) change.new_ = text2html(change.new);
+ html = true;
+ }
+ // format attendees struct
+ else if (prop == 'attendees') {
+ if (change.old) change.old_ = event_attendee_html(change.old);
+ if (change.new) change.new_ = event_attendee_html($.extend({}, change.old || {}, change.new));
+ html = true;
+ }
+ // localize priority values
+ else if (prop == 'priority') {
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ if (change.old) change.old_ = change.old + ' ' + (priolabels[change.old] || '');
+ if (change.new) change.new_ = change.new + ' ' + (priolabels[change.new] || '');
+ }
+ // localize status
+ else if (prop == 'status') {
+ var status_lc = String(event.status).toLowerCase();
+ if (change.old) change.old_ = rcmail.gettext(String(change.old).toLowerCase(), 'calendar');
+ if (change.new) change.new_ = rcmail.gettext(String(change.new).toLowerCase(), 'calendar');
+ }
+
+ // format attachments struct
+ if (prop == 'attachments') {
+ if (change.old) event_show_attachments([change.old], row.children('.event-text-old'), event, false);
+ else row.children('.event-text-old').html('--');
+ if (change.new) event_show_attachments([$.extend({}, change.old || {}, change.new)], row.children('.event-text-new'), event, false);
+ else row.children('.event-text-new').html('--');
+ // remove click handler as we're currentyl not able to display the according attachment contents
+ $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+ }
+ else if (change.diff_) {
+ row.children('.event-text-diff').html(change.diff_);
+ row.children('.event-text-old, .event-text-new').hide();
+ }
+ else {
+ if (!html) {
+ // escape HTML characters
+ change.old_ = Q(change.old_ || change.old || '--')
+ change.new_ = Q(change.new_ || change.new || '--')
+ }
+ row.children('.event-text-old').html(change.old_ || change.old || '--');
+ row.children('.event-text-new').html(change.new_ || change.new || '--');
+ }
+
+ // display index number
+ if (typeof change.index != 'undefined') {
+ row.find('.index').html('(' + change.index + ')');
+ }
+
+ row.show().data('set', true);
+
+ // hide event-date line
+ if (prop == 'start' || prop == 'end')
+ $('.event-date', $dialog).hide();
+ });
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('eventdiff','calendar').replace('$rev', data.rev) + ' - ' + event.title,
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 450
+ }).show();
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+ };
+
+ // exports
+ this.event_show_diff = event_show_diff;
+ this.event_show_dialog = event_show_dialog;
+ this.event_history_dialog = event_history_dialog;
+ this.render_event_changelog = render_event_changelog;
+
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
@@ -2055,7 +2384,7 @@ function rcube_calendar_ui(settings)
var dialog_check = function(e)
{
var showd = $("#eventshow");
- if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
+ if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
@@ -2589,6 +2918,23 @@ function rcube_calendar_ui(settings)
};
+ // download the selected event as iCal
+ this.event_download = function(event)
+ {
+ if (event && event.id) {
+ rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 });
+ }
+ };
+
+ // open the message compose step with a calendar_event parameter referencing the selected event.
+ // the server-side plugin hook will pick that up and attach the event to the message.
+ this.event_sendbymail = function(event, e)
+ {
+ if (event && event.id) {
+ rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
+ }
+ };
+
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
@@ -3452,9 +3798,11 @@ function rcube_calendar_ui(settings)
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
- h = $('#event-rsvp').show().height();
- h -= $(this).closest('.event-line').toggle().height();
- me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ h = -$(this).closest('.event-line').toggle().height();
+ $('#event-rsvp').slideDown(300, function() {
+ h += $(this).height();
+ me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ });
return false;
})
@@ -3508,7 +3856,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
-
+ rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
+ rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
+ rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
+
// search and export events
rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
@@ -3528,6 +3879,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
+ rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
+ rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
+ rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
// let's go
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 4c4f5161..e213a939 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -100,7 +100,8 @@ abstract class calendar_driver
public $attendees = false;
public $freebusy = false;
public $attachments = false;
- public $undelete = false; // event undelete action
+ public $undelete = false;
+ public $history = false;
public $categoriesimmutable = false;
public $alarm_types = array('DISPLAY');
public $alarm_absolute = true;
@@ -405,6 +406,77 @@ abstract class calendar_driver
return false;
}
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of changes, each as a hash array:
+ * rev: Revision number
+ * type: Type of the change (create, update, move, delete)
+ * date: Change date
+ * user: The user who executed the change
+ * ip: Client IP
+ * destination: Destination calendar for 'move' type
+ */
+ public function get_event_changelog($event)
+ {
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array:
+ * property: Revision number
+ * old: Old property value
+ * new: Updated property value
+ */
+ public function get_event_diff($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see self::get_event()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ return false;
+ }
+
+
/**
* Callback function to produce driver-specific calendar create/edit form
*
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 706c3cd3..ba718462 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -30,6 +30,7 @@ class kolab_calendar extends kolab_storage_folder_api
public $readonly = true;
public $attachments = true;
public $alarms = false;
+ public $history = false;
public $subscriptions = true;
public $categories = array();
public $storage;
@@ -589,53 +590,8 @@ class kolab_calendar extends kolab_storage_folder_api
{
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
-/*
- // convert from DateTime to unix timestamp
- if (is_a($record['start'], 'DateTime'))
- $record['start'] = $record['start']->format('U');
- if (is_a($record['end'], 'DateTime'))
- $record['end'] = $record['end']->format('U');
-*/
- // all-day events go from 12:00 - 13:00
- if ($record['end'] <= $record['start'] && $record['allday']) {
- $record['end'] = clone $record['start'];
- $record['end']->add(new DateInterval('PT1H'));
- }
- if (!empty($record['_attachments'])) {
- foreach ($record['_attachments'] as $key => $attachment) {
- if ($attachment !== false) {
- if (!$attachment['name'])
- $attachment['name'] = $key;
-
- unset($attachment['path'], $attachment['content']);
- $attachments[] = $attachment;
- }
- }
-
- $record['attachments'] = $attachments;
- }
-
- // Roundcube only supports one category assignment
- if (is_array($record['categories']))
- $record['categories'] = $record['categories'][0];
-
- // the cancelled flag transltes into status=CANCELLED
- if ($record['cancelled'])
- $record['status'] = 'CANCELLED';
-
- // The web client only supports DISPLAY type of alarms
- if (!empty($record['alarms']))
- $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
-
- // remove empty recurrence array
- if (empty($record['recurrence']))
- unset($record['recurrence']);
-
- // remove internals
- unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
-
- return $record;
+ return kolab_driver::to_rcube_event($record);
}
/**
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ba634cb5..f921b530 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -47,6 +47,7 @@ class kolab_driver extends calendar_driver
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
+ private $bonnie_api = false;
/**
* Default constructor
@@ -69,6 +70,10 @@ class kolab_driver extends calendar_driver
$this->alarm_absolute = false;
}
+ // get configuration for the Bonnie API
+ if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
+ $this->bonnie_api = new kolab_bonnie_api($bonnie_config);
+
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
@@ -164,6 +169,7 @@ class kolab_driver extends calendar_driver
'active' => $cal->is_active(),
'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
+ 'history' => false,
'virtual' => false,
'readonly' => true,
'group' => 'other',
@@ -192,6 +198,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
@@ -222,6 +229,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
@@ -252,6 +260,7 @@ class kolab_driver extends calendar_driver
'readonly' => true,
'default' => false,
'children' => false,
+ 'history' => false,
);
}
}
@@ -1285,6 +1294,265 @@ class kolab_driver extends calendar_driver
exit;
}
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ public static function to_rcube_event($record)
+ {
+ $record['id'] = $record['uid'];
+
+ // all-day events go from 12:00 - 13:00
+ if ($record['end'] <= $record['start'] && $record['allday']) {
+ $record['end'] = clone $record['start'];
+ $record['end']->add(new DateInterval('PT1H'));
+ }
+
+ if (!empty($record['_attachments'])) {
+ foreach ($record['_attachments'] as $key => $attachment) {
+ if ($attachment !== false) {
+ if (!$attachment['name'])
+ $attachment['name'] = $key;
+
+ unset($attachment['path'], $attachment['content']);
+ $attachments[] = $attachment;
+ }
+ }
+
+ $record['attachments'] = $attachments;
+ }
+
+ // Roundcube only supports one category assignment
+ if (is_array($record['categories']))
+ $record['categories'] = $record['categories'][0];
+
+ // the cancelled flag transltes into status=CANCELLED
+ if ($record['cancelled'])
+ $record['status'] = 'CANCELLED';
+
+ // The web client only supports DISPLAY type of alarms
+ if (!empty($record['alarms']))
+ $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
+
+ // remove empty recurrence array
+ if (empty($record['recurrence']))
+ unset($record['recurrence']);
+
+ // remove internals
+ unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
+
+ return $record;
+ }
+
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties
+ *
+ * @return array List of changes, each as a hash array
+ * @see calendar_driver::get_event_changelog()
+ */
+ public function get_event_changelog($event)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ $result = $this->bonnie_api->changelog('event', $uid, $folder);
+ if (is_array($result) && $result['uid'] == $uid) {
+ return $result['changes'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array
+ * @see calendar_driver::get_event_diff()
+ */
+ public function get_event_diff($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->diff('event', $uid, $rev, $folder);
+ if (is_array($result) && $result['uid'] == $uid) {
+ $result['rev'] = $rev;
+
+ $keymap = array(
+ 'dtstart' => 'start',
+ 'dtend' => 'end',
+ 'dstamp' => 'changed',
+ 'summary' => 'title',
+ 'alarm' => 'alarms',
+ 'attendee' => 'attendees',
+ 'attach' => 'attachments',
+ 'rrule' => 'recurrence',
+ 'transparency' => 'free_busy',
+ 'classification' => 'sensitivity',
+ 'lastmodified-date' => 'changed',
+ );
+ $prop_keymaps = array(
+ 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
+ 'attendees' => array('partstat' => 'status'),
+ );
+ $special_changes = array();
+
+ // map kolab event properties to keys the client expects
+ array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
+ if (array_key_exists($change['property'], $keymap)) {
+ $change['property'] = $keymap[$change['property']];
+ }
+ // translate free_busy values
+ if ($change['property'] == 'free_busy') {
+ $change['old'] = $old['old'] ? 'free' : 'busy';
+ $change['new'] = $old['new'] ? 'free' : 'busy';
+ }
+ // map alarms trigger value
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']) && is_array($change['old']['trigger']))
+ $change['old']['trigger'] = $change['old']['trigger']['value'];
+ if (is_array($change['new']) && is_array($change['new']['trigger']))
+ $change['new']['trigger'] = $change['new']['trigger']['value'];
+ }
+ // make all property keys uppercase
+ if ($change['property'] == 'recurrence') {
+ $special_changes['recurrence'] = $i;
+ foreach (array('old','new') as $m) {
+ if (is_array($change[$m])) {
+ $props = array();
+ foreach ($change[$m] as $k => $v)
+ $props[strtoupper($k)] = $v;
+ $change[$m] = $props;
+ }
+ }
+ }
+ // map property keys names
+ if (is_array($prop_keymaps[$change['property']])) {
+ foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+ if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
+ $change['old'][$dest] = $change['old'][$k];
+ unset($change['old'][$k]);
+ }
+ if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
+ $change['new'][$dest] = $change['new'][$k];
+ unset($change['new'][$k]);
+ }
+ }
+ }
+
+ if ($change['property'] == 'exdate') {
+ $special_changes['exdate'] = $i;
+ }
+ else if ($change['property'] == 'rdate') {
+ $special_changes['rdate'] = $i;
+ }
+ });
+
+ // merge some recurrence changes
+ foreach (array('exdate','rdate') as $prop) {
+ if (array_key_exists($prop, $special_changes)) {
+ $exdate = $result['changes'][$special_changes[$prop]];
+ if (array_key_exists('recurrence', $special_changes)) {
+ $recurrence = &$result['changes'][$special_changes['recurrence']];
+ }
+ else {
+ $i = count($result['changes']);
+ $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
+ $recurrence = &$result['changes'][$i]['recurrence'];
+ }
+ $key = strtoupper($prop);
+ $recurrence['old'][$key] = $exdate['old'];
+ $recurrence['new'][$key] = $exdate['new'];
+ unset($result['changes'][$special_changes[$prop]]);
+ }
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param array Hash array with event properties
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see calendar_driver::get_event_revison()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $calid = $event['calendar'];
+ list($uid, $folder) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->get('event', $uid, $rev, $folder);
+ if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+ $format = kolab_format::factory('event');
+ $format->load($result['xml']);
+ $event = $format->to_array();
+
+ if ($format->is_valid()) {
+ if ($result['folder'] && ($cal = $this->get_calendar(kolab_storage::id_encode($result['folder'])))) {
+ $event['calendar'] = $cal->id;
+ }
+ else {
+ $event['calendar'] = $calid;
+ }
+
+ $event['rev'] = $result['rev'];
+ return self::to_rcube_event($event);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to resolved the given event identifier into uid and folder
+ *
+ * @return array (uid,folder) tuple
+ */
+ private function _resolve_event_identity($event)
+ {
+ $folder = null;
+ if (is_array($event)) {
+ $uid = $event['id'] ?: $event['uid'];
+ if ($cal = $this->get_calendar($event['calendar']) && !($cal instanceof kolab_invitation_calendar)) {
+ $folder = $cal->name;
+ }
+ }
+ else {
+ $uid = $event;
+ }
+
+ // FIXME: hard-code UID for static Bonnie API demo
+ $demo_uids = $this->rc->config->get('kolab_static_bonnie_uids', array('0015c5fe-9baf-0561-11e3-d584fa2894b7'));
+ if (!in_array($uid, $demo_uids))
+ $uid = reset($demo_uids);
+
+ return array($uid, $folder);
+ }
+
/**
* Callback function to produce driver-specific calendar create/edit form
*
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index bcfc7721..58c22963 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -391,9 +391,7 @@ class kolab_user_calendar extends kolab_calendar
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
- // TODO: implement this
-
- return $record;
+ return kolab_driver::to_rcube_event($record);
}
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 6e165ca2..df159576 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -97,6 +97,7 @@ class calendar_ui
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
$this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
$this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
+ $this->cal->register_handler('plugin.event_changelog_table', array($this, 'event_changelog_table'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
@@ -840,6 +841,22 @@ class calendar_ui
return $table->show($attrib);
}
+ /**
+ * Table oultine for event changelog display
+ */
+ function event_changelog_table($attrib = array())
+ {
+ $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
+ $table->add_header('diff', '');
+ $table->add_header('revision', $this->cal->gettext('revision'));
+ $table->add_header('date', $this->cal->gettext('date'));
+ $table->add_header('user', $this->cal->gettext('user'));
+ $table->add_header('operation', $this->cal->gettext('operation'));
+ $table->add_header('actions', ' ');
+
+ return $table->show($attrib);
+ }
+
/**
*
*/
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index f7af6723..7d1e7d20 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -81,7 +81,12 @@ $labels['private'] = 'private';
$labels['confidential'] = 'confidential';
$labels['alarms'] = 'Reminder';
$labels['comment'] = 'Comment';
+$labels['created'] = 'Created';
+$labels['changed'] = 'Last Modified';
+$labels['unknown'] = 'Unknown';
+$labels['eventoptions'] = 'Options';
$labels['generated'] = 'generated at';
+$labels['eventhistory'] = 'History';
$labels['printdescriptions'] = 'Print descriptions';
$labels['parentcalendar'] = 'Insert inside';
$labels['searchearlierdates'] = '« Search for earlier events';
@@ -253,6 +258,24 @@ $labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
+// history dialog
+$labels['eventchangelog'] = 'Change History';
+$labels['eventdiff'] = 'Changes from revisions $rev';
+$labels['revision'] = 'Revision';
+$labels['user'] = 'User';
+$labels['operation'] = 'Action';
+$labels['actionappend'] = 'Saved';
+$labels['actionmove'] = 'Moved';
+$labels['actiondelete'] = 'Deleted';
+$labels['compare'] = 'Compare';
+$labels['showrevision'] = 'Show this version';
+$labels['restore'] = 'Restore this version';
+$labels['eventnotfound'] = 'Failed to load event data';
+$labels['eventchangelognotavailable'] = 'Change history is not available for this event';
+$labels['eventdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['eventrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.';
+
+
// (hidden) titles and labels for accessibility annotations
$labels['arialabelminical'] = 'Calendar date selection';
$labels['arialabelcalendarview'] = 'Calendar view';
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 28556352..065cb68c 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -526,6 +526,10 @@ a.miniColors-trigger {
/* jQuery UI overrides */
+.calendarmain .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+ float: left;
+}
+
#eventshow h1 {
font-size: 20px;
margin: 0.1em 0 0.4em 0;
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index 07839f05..2df1ce71 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -115,6 +115,13 @@
+
+
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 391f4c1c..84991772 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -663,53 +663,160 @@ a.miniColors-trigger {
/* jQuery UI overrides */
-#eventshow h1 {
+.calendarmain .eventdialog h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
-#eventshow label,
-#eventshow h5.label {
+.calendarmain .eventdialog label,
+.calendarmain .eventdialog h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
-#eventshow {
+.calendarmain .eventdialog label span.index,
+.calendarmain .eventdialog h5.label .index {
+ vertical-align: inherit;
+ margin-left: 0.6em;
+}
+
+.calendarmain .eventdialog {
margin: 0 -0.2em;
}
-#eventshow.status-cancelled {
+.calendarmain .eventdialog.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
-#eventshow.sensitivity-private {
+.calendarmain .eventdialog.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
-#eventshow.sensitivity-confidential {
+.calendarmain .eventdialog.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
-.sensitivity-private #event-title {
+.calendarmain .sensitivity-private #event-title {
margin-right: 50px;
}
-.sensitivity-confidential #event-title {
+.calendarmain .sensitivity-confidential #event-title {
margin-right: 60px;
}
-#eventshow div.event-line {
+.calendarmain .eventdialog div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
-#eventshow div.event-line a.iconbutton {
+.calendarmain .eventdialog div.event-line a.iconbutton {
margin-left: 0.5em;
line-height: 17px;
}
+.calendarmain .eventdialog div.event-line span.event-text + label {
+ margin-left: 2em;
+}
+
+.calendarmain .eventdialog #event-created-changed {
+ margin-top: 0.6em;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff {
+ padding: 2px;
+}
+
+.eventdialog .event-text-diff del,
+.eventdialog .event-text-diff ins {
+ text-decoration: none;
+ color: inherit;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-diff del {
+ background-color: #fdd;
+ /* text-decoration: line-through; */
+}
+
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff ins {
+ background-color: #dfd;
+}
+
+#eventdiff .attachmentslist li a,
+#eventdiff .attachmentslist li a:hover {
+ cursor: default;
+ text-decoration: none;
+}
+
+#eventhistory .loading {
+ color: #666;
+ margin: 1em 0;
+ padding: 1px 0 2px 24px;
+ background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+#eventhistory .compare-button {
+ margin: 4px 0;
+}
+
+#event-changelog-table tbody td {
+ padding: 4px 7px;
+ vertical-align: middle;
+}
+
+#event-changelog-table tbody tr.undisclosed td.date,
+#event-changelog-table tbody tr.undisclosed td.user {
+ font-style: italic;
+}
+
+#event-changelog-table .diff {
+ width: 4em;
+ padding: 2px;
+}
+
+#event-changelog-table .revision {
+ width: 5em;
+}
+
+#event-changelog-table .date {
+ width: 11em;
+}
+
+#event-changelog-table .user {
+ width: auto;
+}
+
+#event-changelog-table .operation {
+ width: 15%;
+}
+
+#event-changelog-table .actions {
+ width: 50px;
+ text-align: right;
+ padding: 4px;
+}
+
+#event-changelog-table td a.iconbutton.restore,
+#event-changelog-table td a.iconbutton.preview {
+ background-image: url(images/calendars.png);
+ background-position: 1px -147px;
+}
+
+#event-changelog-table td a.iconbutton.restore {
+ background-image: url(images/calendars.png);
+ background-position: 1px -167px;
+}
+
+#event-changelog-table tr.first td a.iconbutton {
+ opacity: 0.3;
+ cursor: default;
+}
+
#event-partstat .changersvp {
cursor: pointer;
color: #333;
@@ -745,7 +852,7 @@ a.miniColors-trigger {
}
div.form-section,
-#eventshow div.event-section,
+.calendarmain .eventdialog div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.6em;
@@ -757,7 +864,7 @@ div.form-section,
border-bottom: 2px solid #fafafa;
}
-#eventshow label,
+.calendarmain .eventdialog label,
#eventedit label,
.form-section label {
display: inline-block;
@@ -830,7 +937,7 @@ td.topalign {
#event-rsvp,
#edit-attendees-notify {
- margin: 0.3em 0;
+ margin: 0.6em 0 0.3em 0;
padding: 0.5em;
}
@@ -1186,13 +1293,13 @@ td.topalign {
}
a.dropdown-link {
- font-size: 12px;
+ font-size: 11px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
- font-size: 11px;
+ font-size: 10px;
color: #666;
}
@@ -1201,7 +1308,10 @@ a.dropdown-link:after {
}
.ui-dialog-buttonset a.dropdown-link {
- margin-right: 1em;
+ position: relative;
+ top: 2px;
+ margin: 0 1em;
+ color: #333;
}
#calendarsidebar .ui-datepicker-calendar {
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index bf84f3ac..5e53cb63 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 4f3228c0..50ad616b 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -77,7 +77,7 @@
-
+
Event Title
Location
From-To
@@ -136,10 +136,109 @@
+
+
+
+
+
+
+
+
+
+
Event Title
+
+
+
+
+
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+ ⇢
+
+
+
+
+
@@ -223,6 +322,11 @@
+
+
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 9c507a37..d267da53 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -746,7 +746,17 @@ class libcalendaring extends rcube_plugin
$until = $this->gettext('forever');
}
- return rtrim($freq . $details . ', ' . $until);
+ $except = '';
+ if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
+ $format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+ $exdates = array_map(
+ function($dt) use ($format) { return format_date($dt, $format); },
+ array_slice($rrule['EXDATE'], 0, 10)
+ );
+ $except = '; ' . $this->gettext('except') . ' ' . join(', ');
+ }
+
+ return rtrim($freq . $details . ', ' . $until . $except);
}
/**
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c7175..26e4ae43 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -66,6 +66,7 @@ $labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'except';
// itip related labels
$labels['itipinvitation'] = 'Invitation to';