From 1ce100b000be67213ad0834b525a6bca81ed57df Mon Sep 17 00:00:00 2001 From: "Bogomil Shopov (Kolab Systems)" Date: Wed, 13 Jul 2011 17:52:18 +0300 Subject: [PATCH] Printing Calendars stage 1 --- plugins/calendar/calendar.php | 30 + plugins/calendar/calendar_ui.js | 6 +- plugins/calendar/lib/calendar_ui.php | 2 +- plugins/calendar/print.js | 1336 +++++++++++++++++ .../skins/default/fullcalendar.printl.css | 97 ++ .../skins/default/templates/calendar.html | 3 - .../skins/default/templates/print.html | 31 + 7 files changed, 1498 insertions(+), 7 deletions(-) create mode 100644 plugins/calendar/print.js create mode 100644 plugins/calendar/skins/default/fullcalendar.printl.css create mode 100644 plugins/calendar/skins/default/templates/print.html diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 91975a3d..a5fdd3d4 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -111,6 +111,7 @@ class calendar extends rcube_plugin $this->register_action('freebusy-status', array($this, 'freebusy_status')); $this->register_action('freebusy-times', array($this, 'freebusy_times')); $this->register_action('randomdata', array($this, 'generate_randomdata')); + $this->register_action('print',array($this,'print_view')); } else if ($this->rc->task == 'settings') { // add hooks for Calendar settings @@ -1187,5 +1188,34 @@ class calendar extends rcube_plugin echo json_encode($result); exit; } + + /** + * Handle for printing calendars + */ + + public function print_view() + { + + + $this->rc->output->set_pagetitle($this->gettext('Print')); + $this->rc->output->set_env('nview', get_input_value('nview', RCUBE_INPUT_GPC)); + // Add CSS stylesheets to the page header + $skin = $this->rc->config->get('skin'); + $this->include_stylesheet('skins/' . $skin . '/fullcalendar.css'); + $this->include_stylesheet('skins/' . $skin . '/fullcalendar.printl.css'); + + // Add JS files to the page header + $this->include_script('print.js'); + + $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css')); + $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list')); + $this->register_handler('plugin.calendar_select', array($this->ui, 'calendar_select')); + + + // $this->rc->output->add_label('low','normal','high','delete','cancel','uploading','noemailwarning'); + + $this->rc->output->send("calendar.print"); + } +} diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 35a67c76..867516dc 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -733,9 +733,9 @@ function rcube_calendar_ui(settings) /*** public methods ***/ //public method to show the print dialog. - this.print_calendars = function() { + this.print_calendars = function(view) { - calendars_print_dialog('new'); + window.open ("?_task=calendar&_action=print&nview="+view.name+"","rc_print_calendars","width=670"); }; @@ -1335,7 +1335,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // configure toolbar buttons rcmail.register_command('addevent', function(){ cal.add_event(); }, true); - rcmail.register_command('print', function(){ cal.print_calendars(); }, true); + rcmail.register_command('print', function(){ cal.print_calendars($('#calendar').fullCalendar('getView')); }, true); // configure list operations rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 6fefdf7e..26d9ece9 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -58,7 +58,7 @@ class calendar_ui { $skin = $this->rc->config->get('skin'); $this->calendar->include_stylesheet('skins/' . $skin . '/fullcalendar.css'); - $this->calendar->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css'); + $this->calendar->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css'); } /** diff --git a/plugins/calendar/print.js b/plugins/calendar/print.js new file mode 100644 index 00000000..96635459 --- /dev/null +++ b/plugins/calendar/print.js @@ -0,0 +1,1336 @@ +/* + +-------------------------------------------------------------------------+ + | Javascript for the Calendar Plugin | + | Version 0.3 beta | + | | + | This program is free software; you can redistribute it and/or modify | + | it under the terms of the GNU General Public License version 2 | + | as published by the Free Software Foundation. | + | | + | This program is distributed in the hope that it will be useful, | + | but WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | + | GNU General Public License for more details. | + | | + | You should have received a copy of the GNU General Public License along | + | with this program; if not, write to the Free Software Foundation, Inc., | + | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | + | | + +-------------------------------------------------------------------------+ + | Author: Lazlo Westerhof | + | Thomas Bruederli | + +-------------------------------------------------------------------------+ +*/ + +// Roundcube calendar UI client class +function rcube_calendar_ui(settings) +{ + // extend base class + rcube_calendar.call(this, settings); + + /*** member vars ***/ + this.selected_event = null; + this.selected_calendar = null; + this.search_request = null; + this.search_source = null; + this.eventcount = []; + this.saving_lock = null; + + + /*** private vars ***/ + var me = this; + var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0); + var day_clicked = day_clicked_ts = 0; + var ignore_click = false; + var event_attendees = null; + var attendees_list; + + // general datepicker settings + var datepicker_settings = { + // translate from fullcalendar format to datepicker format + dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'), + firstDay : settings['first_day'], + dayNamesMin: settings['days_short'], + monthNames: settings['months'], + monthNamesShort: settings['months'], + changeMonth: false, + showOtherMonths: true, + selectOtherMonths: true + }; + + + /*** private methods ***/ + + var Q = this.quote_html; + + // php equivalent + var nl2br = function(str) + { + return String(str).replace(/\n/g, "
"); + }; + + // + var explode_quoted_string = function(str, delimiter) + { + var result = [], + strlen = str.length, + q, p, i; + + for (q = p = i = 0; i < strlen; i++) { + if (str[i] == '"' && str[i-1] != '\\') { + q = !q; + } + else if (!q && str[i] == delimiter) { + result.push(str.substring(p, i)); + p = i + 1; + } + } + + result.push(str.substr(p)); + return result; + }; + + // from time and date strings to a real date object + var parse_datetime = function(time, date) + { + // we use the utility function from datepicker to parse dates + var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date(); + + var time_arr = time.replace(/\s*[ap]m?/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/); + if (!isNaN(time_arr[0])) { + date.setHours(time_arr[0]); + if (time.match(/pm?/i) && date.getHours() < 12) + date.setHours(parseInt(time_arr[0]) + 12); + else if (date.getHours() == 12) + date.setHours(0); + } + if (!isNaN(time_arr[1])) + date.setMinutes(time_arr[1]); + + return date; + }; + + // convert the given Date object into a unix timestamp respecting browser's and user's timezone settings + var date2unixtime = function(date) + { + return date.getTime()/1000 + gmt_offset * 3600; + }; + + var fromunixtime = function(ts) + { + ts -= gmt_offset * 3600; + return new Date(ts * 1000); + } + + // create a nice human-readable string for the date/time range + var event_date_text = function(event) + { + var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000; + if (event.allDay) + fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' — ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : ''); + else if (duration < 86400 && event.start.getDay() == event.end.getDay()) + fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' — ' + + $.fullCalendar.formatDate(event.end, settings['time_format']); + else + fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' — ' + + $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']); + + return fromto; + }; + + var load_attachment = function(event, att) + { + var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar); + + // open attachment in frame if it's of a supported mimetype + if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) { + rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment'); + if (rcmail.attachment_win) { + window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10); + return; + } + } + + rcmail.goto_url('get-attachment', qstring+'&_download=1', false); + }; + + // build event attachments list + var event_show_attachments = function(list, container, event, edit) + { + var i, id, len, img, content, li, elem, + ul = document.createElement('UL'); + + for (i=0, len=list.length; i 0) { + $('#event-attachments').show(); + } + } + else if (calendar.attachments) { + // fetch attachments, some drivers doesn't set 'attachments' popr of the event + } + + var buttons = {}; + if (calendar.editable && event.editable !== false) { + buttons[rcmail.gettext('edit', 'calendar')] = function() { + event_edit_dialog('edit', event); + }; + buttons[rcmail.gettext('remove', 'calendar')] = function() { + me.delete_event(event); + $dialog.dialog('close'); + }; + } + else { + buttons[rcmail.gettext('close', 'calendar')] = function(){ + $dialog.dialog('close'); + }; + } + + // open jquery UI dialog + $dialog.dialog({ + modal: false, + resizable: true, + title: null, + close: function() { + $dialog.dialog('destroy').hide(); + }, + buttons: buttons, + minWidth: 320, + width: 420 + }).show(); +/* + // 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()); +*/ + }; + + + + + + // bring up the event dialog (jquery-ui popup) + var event_edit_dialog = function(action, event) + { + // close show dialog first + $("#eventshow").dialog('close'); + + var $dialog = $("#eventedit"); + var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' }; + me.selected_event = event; + + // reset dialog first, enable/disable fields according to editable state + $('#eventtabs').get(0).reset(); + $('#calendar-select')[(action == 'new' ? 'show' : 'hide')](); + + // event details + var title = $('#edit-title').val(event.title); + var location = $('#edit-location').val(event.location); + var description = $('#edit-description').val(event.description); + var categories = $('#edit-categories').val(event.categories); + var calendars = $('#edit-calendar').val(event.calendar); + var freebusy = $('#edit-free-busy').val(event.free_busy); + var priority = $('#edit-priority').val(event.priority); + var sensitivity = $('#edit-sensitivity').val(event.sensitivity); + + var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); + var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); + var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); + var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); + var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); + var allday = $('#edit-allday').get(0); + + if (event.allDay) { + starttime.val("00:00").hide(); + endtime.val("23:59").hide(); + allday.checked = true; + } + else { + allday.checked = false; + } + + // set alarm(s) + // TODO: support multiple alarm entries + if (event.alarms) { + if (typeof event.alarms == 'string') + event.alarms = event.alarms.split(';'); + + for (var alarm, i=0; i < event.alarms.length; i++) { + alarm = String(event.alarms[i]).split(':'); + if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY'; + $('select.edit-alarm-type').val(alarm[1]); + + if (alarm[0].match(/@(\d+)/)) { + var ondate = fromunixtime(parseInt(RegExp.$1)); + $('select.edit-alarm-offset').val('@'); + $('input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format'])); + $('input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format'])); + } + else if (alarm[0].match(/([-+])(\d+)([MHD])/)) { + $('input.edit-alarm-value').val(RegExp.$2); + $('select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3); + } + } + } + // set correct visibility by triggering onchange handlers + $('select.edit-alarm-type, select.edit-alarm-offset').change(); + + // enable/disable alarm property according to backend support + $('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')](); + + // set recurrence form + var recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change(); + var interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1); + var rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1); + var rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : ''); + $('input.edit-recurrence-until:checked').prop('checked', false); + + var weekdays = ['SU','MO','TU','WE','TH','FR','SA']; + var rrepeat_id = '#edit-recurrence-repeat-forever'; + if (event.recurrence && event.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count'; + else if (event.recurrence && event.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until'; + $(rrepeat_id).prop('checked', true); + + if (event.recurrence && event.recurrence.BYDAY && event.recurrence.FREQ == 'WEEKLY') { + var wdays = event.recurrence.BYDAY.split(','); + $('input.edit-recurrence-weekly-byday').val(wdays); + } + if (event.recurrence && event.recurrence.BYMONTHDAY) { + $('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(',')); + $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']); + } + if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) { + var byday, section = event.recurrence.FREQ.toLowerCase(); + if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) { + $('#edit-recurrence-'+section+'-prefix').val(byday[1]); + $('#edit-recurrence-'+section+'-byday').val(byday[2]); + } + $('input.edit-recurrence-'+section+'-mode').val(['BYDAY']); + } + else if (event.start) { + $('#edit-recurrence-monthly-byday').val(weekdays[event.start.getDay()]); + } + if (event.recurrence && event.recurrence.BYMONTH) { + $('input.edit-recurrence-yearly-bymonth').val(String(event.recurrence.BYMONTH).split(',')); + } + else if (event.start) { + $('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]); + } + + // show warning if editing a recurring event + if (event.id && event.recurrence) { + $('#edit-recurring-warning').show(); + $('input.edit-recurring-savemode[value="all"]').prop('checked', true); + } + else + $('#edit-recurring-warning').hide(); + + // attendees + event_attendees = []; + attendees_list = $('#edit-attendees-table > tbody').html(''); + if (calendar.attendees && event.attendees) { + for (var j=0; j < event.attendees.length; j++) + add_attendee(event.attendees[j]); + } + + // attachments + if (calendar.attachments) { + rcmail.enable_command('remove-attachment', !calendar.readonly); + rcmail.env.deleted_attachments = []; + // we're sharing some code for uploads handling with app.js + rcmail.env.attachments = []; + rcmail.env.compose_id = event.id; // for rcmail.async_upload_form() + + if ($.isArray(event.attachments)) { + event_show_attachments(event.attachments, $('#edit-attachments'), event, true); + } + else { + $('#edit-attachments > ul').empty(); + // fetch attachments, some drivers doesn't set 'attachments' array for event + } + } + + // buttons + var buttons = {}; + + buttons[rcmail.gettext('save', 'calendar')] = function() { + var start = parse_datetime(starttime.val(), startdate.val()); + var end = parse_datetime(endtime.val(), enddate.val()); + + // basic input validatetion + if (start.getTime() > end.getTime()) { + alert(rcmail.gettext('invalideventdates', 'calendar')); + return false; + } + + // post data to server + var data = { + calendar: event.calendar, + start: date2unixtime(start), + end: date2unixtime(end), + allday: allday.checked?1:0, + title: title.val(), + description: description.val(), + location: location.val(), + categories: categories.val(), + free_busy: freebusy.val(), + priority: priority.val(), + sensitivity: sensitivity.val(), + recurrence: '', + alarms: '', + attendees: event_attendees, + deleted_attachments: rcmail.env.deleted_attachments + }; + + // serialize alarm settings + // TODO: support multiple alarm entries + var alarm = $('select.edit-alarm-type').val(); + if (alarm) { + var val, offset = $('select.edit-alarm-offset').val(); + if (offset == '@') + data.alarms = '@' + date2unixtime(parse_datetime($('input.edit-alarm-time').val(), $('input.edit-alarm-date').val())) + ':' + alarm; + else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0) + data.alarms = offset[0] + val + offset[1] + ':' + alarm; + } + + // uploaded attachments list + var attachments = []; + for (var i in rcmail.env.attachments) + if (i.match(/^rcmfile([0-9a-z]+)/)) + attachments.push(RegExp.$1); + data.attachments = attachments; + + // gather recurrence settings + var freq; + if ((freq = recurrence.val()) != '') { + data.recurrence = { + FREQ: freq, + INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val() + }; + + var until = $('input.edit-recurrence-until:checked').val(); + if (until == 'count') + data.recurrence.COUNT = rrtimes.val(); + else if (until == 'until') + data.recurrence.UNTIL = date2unixtime(parse_datetime(endtime.val(), rrenddate.val())); + + if (freq == 'WEEKLY') { + var byday = []; + $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); }); + if (byday.length) + data.recurrence.BYDAY = byday.join(','); + } + else if (freq == 'MONTHLY') { + var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = []; + if (mode == 'BYMONTHDAY') { + $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); }); + if (bymonday.length) + data.recurrence.BYMONTHDAY = bymonday.join(','); + } + else + data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val(); + } + else if (freq == 'YEARLY') { + var byday, bymonth = []; + $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); }); + if (bymonth.length) + data.recurrence.BYMONTH = bymonth.join(','); + if ((byday = $('#edit-recurrence-yearly-byday').val())) + data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday; + } + } + + if (event.id) { + data.id = event.id; + if (event.recurrence) + data.savemode = $('input.edit-recurring-savemode:checked').val(); + } + else + data.calendar = calendars.val(); + + update_event(action, data); + $dialog.dialog("close"); + }; + + if (event.id) { + buttons[rcmail.gettext('remove', 'calendar')] = function() { + me.delete_event(event); + $dialog.dialog('close'); + }; + } + + buttons[rcmail.gettext('cancel', 'calendar')] = function() { + $dialog.dialog("close"); + }; + + // show/hide tabs according to calendar's feature support + $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')](); + $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')](); + + // activate the first tab + $('#eventtabs').tabs('select', 0); + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: true, + closeOnEscape: false, + title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'), + close: function() { + $dialog.dialog("destroy").hide(); + }, + buttons: buttons, + minWidth: 500, + width: 580 + }).show(); + + title.select(); + }; + + // add the given list of participants + var add_attendees = function(names) + { + names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); + + // parse name/email pairs + var item, email, name, success = false; + for (var i=0; i < names.length; i++) { + email = name = null; + item = $.trim(names[i]); + + if (!item.length) { + continue; + } // address in brackets without name (do nothing) + else if (item.match(/^<[^@]+@[^>]+>$/)) { + email = item.replace(/[<>]/g, ''); + } // address without brackets and without name (add brackets) + else if (rcube_check_email(item)) { + email = item; + } // address with name + else if (item.match(/([^\s<@]+@[^>]+)>*$/)) { + email = RegExp.$1; + name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); + } + + if (email) { + add_attendee({ email:email, name:name, role:'REQUIRED', status:'unknown' }); + success = true; + } + else { + alert(rcmail.gettext('noemailwarning')); + } + } + + return success; + }; + + // add the given attendee to the list + var add_attendee = function(data) + { + var dispname = (data.email && data.name) ? data.name + ' <' + data.email + '>' : (data.email || data.name); + + // delete icon + var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); + var dellink = '' + icon + ''; + + var html = '' + + '' + Q(dispname) + '' + + '' + '' + '' + + '' + Q(data.status) + '' + + '' + (data.role != 'OWNER' ? dellink : '') + ''; + + $('') + .addClass(String(data.role).toLowerCase()) + .html(html) + .appendTo(attendees_list) + .find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); + + event_attendees.push(data); + }; + + // remove an attendee from the list + var remove_attendee = function(elem, id) + { + $(elem).closest('tr').hide(); + event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) }); + }; + + // post the given event data to server + var update_event = function(action, data) + { + me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata'); + rcmail.http_post('event', { action:action, e:data }); + }; + + // mouse-click handler to check if the show dialog is still open and prevent default action + var dialog_check = function(e) + { + var showd = $("#eventshow"); + if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) { + showd.dialog('close'); + e.stopImmediatePropagation(); + ignore_click = true; + return false; + } + else if (ignore_click) { + window.setTimeout(function(){ ignore_click = false; }, 20); + return false; + } + return true; + }; + + // display confirm dialog when modifying/deleting a recurring event where the user needs to select the savemode + var recurring_edit_confirm = function(event, action) { + var $dialog = $('
').addClass('edit-recurring-warning'); + $dialog.html('
' + + rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + + ''); + + $dialog.find('a.button').button().click(function(e){ + event.savemode = String(this.href).replace(/.+#/, ''); + update_event(action, event); + $dialog.dialog("destroy").hide(); + return false; + }); + + $dialog.dialog({ + modal: true, + width: 420, + dialogClass: 'warning', + title: rcmail.gettext((action == 'remove' ? 'removerecurringevent' : 'changerecurringevent'), 'calendar'), + buttons: [ + { + text: rcmail.gettext('cancel', 'calendar'), + click: function() { + $(this).dialog("close"); + } + } + ], + close: function(){ + $dialog.dialog("destroy").hide(); + fc.fullCalendar('refetchEvents'); + } + }).show(); + + return true; + }; + + + /*** public methods ***/ + //public method to show the print dialog. + + + // public method to bring up the new event dialog + this.add_event = function() { + if (this.selected_calendar) { + var now = new Date(); + var date = fc.fullCalendar('getDate') || now; + date.setHours(now.getHours()+1); + date.setMinutes(0); + var end = new Date(date.getTime()); + end.setHours(date.getHours()+1); + event_edit_dialog('new', { start:date, end:end, allDay:false, calendar:this.selected_calendar }); + } + }; + + // delete the given event after showing a confirmation dialog + this.delete_event = function(event) { + // show extended confirm dialog for recurring events, use jquery UI dialog + if (event.recurrence) + return recurring_edit_confirm({ id:event.id, calendar:event.calendar }, 'remove'); + + // send remove request to plugin + if (confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) { + update_event('remove', { id:event.id, calendar:event.calendar }); + return true; + } + + return false; + }; + + // opens a jquery UI dialog with event properties (or empty for creating a new calendar) + this.calendar_edit_dialog = function(calendar) + { + // close show dialog first + var $dialog = $("#calendarform").dialog('close'); + + if (!calendar) + calendar = { name:'', color:'cc0000' }; + + var form, name, color; + + $dialog.html(rcmail.get_label('loading')); + $.ajax({ + type: 'GET', + dataType: 'html', + url: rcmail.url('calendar'), + data: { action:(calendar.id ? 'form-edit' : 'form-new'), calendar:{ id:calendar.id } }, + success: function(data){ + $dialog.html(data); + form = $('#calendarform > form'); + name = $('#calendar-name').val(calendar.editname || calendar.name); + color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color }); + name.select(); + } + }); + + // dialog buttons + var buttons = {}; + + buttons[rcmail.gettext('save', 'calendar')] = function() { + // form is not loaded + if (!form) + return; + + // TODO: do some input validation + if (!name.val() || name.val().length < 2) { + alert(rcmail.gettext('invalidcalendarproperties', 'calendar')); + name.select(); + return; + } + + // post data to server + var data = form.serializeJSON(); + if (data.color) + data.color = data.color.replace(/^#/, ''); + if (calendar.id) + data.id = calendar.id; + + rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data }); + $dialog.dialog("close"); + }; + + buttons[rcmail.gettext('cancel', 'calendar')] = function() { + $dialog.dialog("close"); + }; + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: true, + title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'), + close: function() { + $dialog.dialog("destroy").hide(); + }, + buttons: buttons, + minWidth: 400, + width: 420 + }).show(); + + }; + + this.calendar_remove = function(calendar) + { + if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) { + rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } }); + return true; + } + return false; + }; + + this.calendar_destroy_source = function(id) + { + if (this.calendars[id]) { + fc.fullCalendar('removeEventSource', this.calendars[id]); + $(rcmail.get_folder_li(id, 'rcmlical')).remove(); + $('#edit-calendar option[value="'+id+'"]').remove(); + delete this.calendars[id]; + } + }; + + + /*** event searching ***/ + + // execute search + this.quicksearch = function() + { + if (rcmail.gui_objects.qsearchbox) { + var q = rcmail.gui_objects.qsearchbox.value; + if (q != '') { + var id = 'search-'+q; + var sources = []; + + if (this._search_message) + rcmail.hide_message(this._search_message); + + for (var sid in this.calendars) { + if (this.calendars[sid] && this.calendars[sid].active) { + fc.fullCalendar('removeEventSource', this.calendars[sid]); + sources.push(sid); + } + } + id += '@'+sources.join(','); + + // just refetch events if query didn't change + if (this.search_request == id) { + fc.fullCalendar('refetchEvents'); + return; + } + // remove old search results + else if (this.search_source) { + fc.fullCalendar('removeEventSource', this.search_source); + } + else { + this.default_view = fc.fullCalendar('getView').name; + } + + // replace event source from fullcalendar + this.search_request = id; + this.search_source = { + url: "./?_task=calendar&_action=search_events&q="+escape(q)+'&source='+escape(sources.join(',')), + editable: false + }; + + fc.fullCalendar('option', 'listSections', 'month'); + fc.fullCalendar('addEventSource', this.search_source); + fc.fullCalendar('changeView', 'table'); + } + else // empty search input equals reset + this.reset_quicksearch(); + } + }; + + // reset search and get back to normal event listing + this.reset_quicksearch = function() + { + $(rcmail.gui_objects.qsearchbox).val(''); + + if (this._search_message) + rcmail.hide_message(this._search_message); + + if (this.search_request) { + // restore original event sources and view mode from fullcalendar + fc.fullCalendar('option', 'listSections', 'smart'); + fc.fullCalendar('removeEventSource', this.search_source); + for (var sid in this.calendars) { + if (this.calendars[sid] && this.calendars[sid].active) + fc.fullCalendar('addEventSource', this.calendars[sid]); + } + if (this.default_view) + fc.fullCalendar('changeView', this.default_view); + + this.search_request = this.search_source = null; + } + }; + + // callback if all sources have been fetched from server + this.events_loaded = function(count) + { + if (this.search_request && !count) + this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice'); + } + + + /*** startup code ***/ + + // create list of event sources AKA calendars + this.calendars = {}; + var li, cal, active, event_sources = []; + for (var id in rcmail.env.calendars) { + cal = rcmail.env.calendars[id]; + this.calendars[id] = $.extend({ + url: "./?_task=calendar&_action=load_events&source="+escape(id), + editable: !cal.readonly, + className: 'fc-event-cal-'+id, + id: id + }, cal); + + if ((active = ($.inArray(String(id), settings.hidden_calendars) < 0))) { + this.calendars[id].active = true; + event_sources.push(this.calendars[id]); + } + + // init event handler on calendar list checkbox + if ((li = rcmail.get_folder_li(id, 'rcmlical'))) { + $('#'+li.id+' input').click(function(e){ + var id = $(this).data('id'); + if (me.calendars[id]) { // add or remove event source on click + var action; + if (this.checked) { + action = 'addEventSource'; + me.calendars[id].active = true; + settings.hidden_calendars = $.map(settings.hidden_calendars, function(v){ return v == id ? null : v; }); + } + else { + action = 'removeEventSource'; + me.calendars[id].active = false; + settings.hidden_calendars.push(id); + } + // just trigger search again (don't save prefs?) + if (me.search_request) { + me.quicksearch(); + } + else { // add/remove event source + fc.fullCalendar(action, me.calendars[id]); + rcmail.save_pref({ name:'hidden_calendars', value:settings.hidden_calendars.join(',') }); + } + } + }).data('id', id).get(0).checked = active; + + $(li).click(function(e){ + var id = $(this).data('id'); + rcmail.select_folder(id, me.selected_calendar, 'rcmlical'); + rcmail.enable_command('calendar-edit','calendar-remove', !me.calendars[id].readonly); + me.selected_calendar = id; + }).data('id', id); + } + + if (!cal.readonly && !this.selected_calendar && (!settings.default_calendar || settings.default_calendar == id)) { + this.selected_calendar = id; + rcmail.enable_command('addevent', true); + } + } + + // initalize the fullCalendar plugin + var fc = $('#calendar').fullCalendar({ + header: { + left: '', + center: 'title', + right: '' + }, + aspectRatio: 1, + ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone + height: '100%', + eventSources: event_sources, + monthNames : settings['months'], + monthNamesShort : settings['months_short'], + dayNames : settings['days'], + dayNamesShort : settings['days_short'], + firstDay : settings['first_day'], + firstHour : settings['first_hour'], + slotMinutes : 60/settings['timeslots'], + timeFormat: { + '': settings['time_format'], + agenda: 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 + list: settings['date_agenda'], + table: settings['date_agenda'] + }, + titleFormat: { + month: 'MMMM yyyy', + week: settings['date_long'].replace(/ yyyy/, '[ yyyy]') + "{ '—' " + settings['date_long'] + "}", + day: 'dddd ' + settings['date_long'], + list: settings['date_long'], + table: settings['date_long'] + }, + listSections: 'smart', + listRange: 60, // show 60 days in list view + tableCols: ['handle', 'date', 'time', 'title', 'location'], + defaultView: rcmail.env.nview, + allDayText: rcmail.gettext('all-day', 'calendar'), + buttonText: { + }, + selectable: false, + selectHelper: true, + loading: function(isLoading) { + this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading); + // trigger callback + if (!isLoading && me.search_request) + me.events_loaded($(this).fullCalendar('clientEvents').length); + }, + // event rendering + eventRender: function(event, element, view) { + if (view.name != 'list' && view.name != 'table') + element.attr('title', event.title); + if (view.name == 'month') { +/* attempt to limit the number of events displayed + (could also be used to init fish-eye-view) + var max = 4; // to be derrived from window size + var sday = event.start.getMonth()*12 + event.start.getDate(); + var eday = event.end.getMonth()*12 + event.end.getDate(); + if (!me.eventcount[sday]) me.eventcount[sday] = 1; + else me.eventcount[sday]++; + if (!me.eventcount[eday]) me.eventcount[eday] = 1; + else if (eday != sday) me.eventcount[eday]++; + + if (me.eventcount[sday] > max || me.eventcount[eday] > max) + return false; +*/ + } + else { + if (event.location) { + element.find('div.fc-event-title').after('
@ ' + Q(event.location) + '
'); + } + if (event.recurrence) + element.find('div.fc-event-time').append(''); + if (event.alarms) + element.find('div.fc-event-time').append(''); + } + }, + // callback for date range selection + select: function(start, end, allDay, e, view) { + var range_select = (!allDay || start.getDate() != end.getDate()) + if (dialog_check(e) && range_select) + event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar }); + if (range_select || ignore_click) + view.calendar.unselect(); + }, + // callback for clicks in all-day box + dayClick: function(date, allDay, e, view) { + var now = new Date().getTime(); + if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) // emulate double-click on day + return event_edit_dialog('new', { start:date, end:date, allDay:allDay, calendar:me.selected_calendar }); + + if (!ignore_click) { + view.calendar.gotoDate(date); + fullcalendar_update(); + if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth()) + view.calendar.select(date, date, allDay); + } + day_clicked = date.getTime(); + day_clicked_ts = now; + }, + // callback when a specific event is clicked + eventClick: function(event) { + event_show_dialog(event); + }, + // callback when an event was dragged and finally dropped + eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) { + if (event.end == null) { + event.end = event.start; + } + // send move request to server + var data = { + id: event.id, + calendar: event.calendar, + start: date2unixtime(event.start), + end: date2unixtime(event.end), + allday: allDay?1:0 + }; + if (event.recurrence) + recurring_edit_confirm(data, 'move'); + else + update_event('move', data); + }, + // callback for event resizing + eventResize: function(event, delta) { + // send resize request to server + var data = { + id: event.id, + calendar: event.calendar, + start: date2unixtime(event.start), + end: date2unixtime(event.end) + }; + if (event.recurrence) + recurring_edit_confirm(data, 'resize'); + else + update_event('resize', data); + }, + viewDisplay: function(view) { + me.eventcount = []; + if (!bw.ie) + window.setTimeout(function(){ $('div.fc-content').css('overflow', view.name == 'month' ? 'auto' : 'hidden') }, 10); + }, + windowResize: function(view) { + me.eventcount = []; + } + }); + + + // event handler for clicks on calendar week cell of the datepicker widget + var init_week_events = function(){ + $('#datepicker table.ui-datepicker-calendar td.ui-datepicker-week-col').click(function(e){ + var base_date = minical.datepicker('getDate'); + var day_off = base_date.getDay() - 1; + if (day_off < 0) day_off = 6; + var base_kw = $.datepicker.iso8601Week(base_date); + var kw = parseInt($(this).html()); + var diff = (kw - base_kw) * 7 * 86400000; + // select monday of the chosen calendar week + var date = new Date(base_date.getTime() - day_off * 86400000 + diff); + fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek'); + minical.datepicker('setDate', date); + window.setTimeout(init_week_events, 10); + }).css('cursor', 'pointer'); + }; + + // initialize small calendar widget using jQuery UI datepicker + var minical = $('#datepicker').datepicker($.extend(datepicker_settings, { + inline: true, + showWeek: true, + changeMonth: false, // maybe enable? + changeYear: false, // maybe enable? + onSelect: function(dateText, inst) { + ignore_click = true; + var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText); + fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true); + window.setTimeout(init_week_events, 10); + }, + onChangeMonthYear: function(year, month, inst) { + window.setTimeout(init_week_events, 10); + var d = minical.datepicker('getDate'); + d.setYear(year); + d.setMonth(month - 1); + minical.data('year', year).data('month', month); + //fc.fullCalendar('gotoDate', d).fullCalendar('setDate', d); + } + })); + window.setTimeout(init_week_events, 10); + + // react on fullcalendar buttons + var fullcalendar_update = function() { + var d = fc.fullCalendar('getDate'); + minical.datepicker('setDate', d); + window.setTimeout(init_week_events, 10); + }; + $("#calendar .fc-button-prev").click(fullcalendar_update); + $("#calendar .fc-button-next").click(fullcalendar_update); + $("#calendar .fc-button-today").click(fullcalendar_update); + + // format time string + var formattime = function(hour, minutes) { + var d = new Date(); + d.setHours(hour); + d.setMinutes(minutes); + return $.fullCalendar.formatDate(d, settings['time_format']) + }; + + // if start date is changed, shift end date according to initial duration + 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, me.settings['date_format'])); + }; + + // init event dialog + $('#eventtabs').tabs({ + show: function(event, ui) { + if (ui.panel.id == 'event-tab-3') + $('#edit-attendee-name').select(); + } + }); + $('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings); + $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); }); + $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); }); + + // configure drop-down menu on time input fields based on jquery UI autocomplete + $('#edit-starttime, #edit-endtime, input.edit-alarm-time') + .attr('autocomplete', "off") + .autocomplete({ + delay: 100, + minLength: 1, + source: function(p, callback) { + /* Time completions */ + var result = []; + var now = new Date(); + var full = p.term - 1 > 0 || p.term.length > 1; + var hours = (full ? parse_datetime(p.term, '') : now).getHours(); + var step = 15; + var minutes = hours * 60 + (full ? 0 : now.getMinutes()); + var min = Math.ceil(minutes / step) * step % 60; + var hour = Math.floor(Math.ceil(minutes / step) * step / 60); + // list hours from 0:00 till now + for (var h = 0; h < hours; h++) + result.push(formattime(h, 0)); + // list 15min steps for the next two hours + for (; h < hour + 2; h++) { + while (min < 60) { + result.push(formattime(h, min)); + min += step; + } + min = 0; + } + // list the remaining hours till 23:00 + while (h < 24) + result.push(formattime((h++), 0)); + return callback(result); + }, + open: function(event, ui) { + // scroll to current time + var widget = $(this).autocomplete('widget'); + var menu = $(this).data('autocomplete').menu; + var val = $(this).val().replace(/^(.+)(am?)/i, '0:$1').replace(/^(.+)(pm?)/i, '1:$1'); + var li, html, offset = 0; + widget.css('width', '7em'); + widget.children().each(function(){ + li = $(this); + html = li.children().first().html().replace(/^(.+)(am?)/i, '0:$1').replace(/^(.+)(pm?)/i, '1:$1'); + if (html < val) + offset += li.height(); + if (html == val) + menu.activate($.Event({ type: 'mouseenter' }), li); + }); + widget.scrollTop(offset - 1); + } + }) + .click(function() { // show drop-down upon clicks + $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); + }); + + // register events on alarm fields + $('select.edit-alarm-type').change(function(){ + $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); + }); + $('select.edit-alarm-offset').change(function(){ + var mode = $(this).val() == '@' ? 'show' : 'hide'; + $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode](); + $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show'); + }); + + // toggle recurrence frequency forms + $('#edit-recurrence-frequency').change(function(e){ + var freq = $(this).val().toLowerCase(); + $('.recurrence-form').hide(); + if (freq) + $('#recurrence-form-'+freq+', #recurrence-form-until').show(); + }); + $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); + + // init attendees autocompletion + rcmail.init_address_input_events($('#edit-attendee-name')); + rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); }); + + $('#edit-attendee-add').click(function(){ + var input = $('#edit-attendee-name'); + if (add_attendees(input.val())) + input.val(''); + }); + + // add proprietary css styles if not IE + if (!bw.ie) + $('div.fc-content').addClass('rcube-fc-content'); + + // hide event dialog when clicking somewhere into document + $(document).bind('mousedown', dialog_check); + +} // end rcube_calendar class + + +/* calendar plugin initialization */ +window.rcmail && rcmail.addEventListener('init', function(evt) { + + // configure toolbar buttons + rcmail.register_command('addevent', function(){ cal.add_event(); }, true); + + // configure list operations + rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); + rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false); + rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false); + + // search and export events + rcmail.register_command('export', function(){ rcmail.goto_url('export_events', { source:cal.selected_calendar }); }, true); + rcmail.register_command('search', function(){ cal.quicksearch(); }, true); + rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true); + + // register callback commands + rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); }); + rcmail.addEventListener('plugin.reload_calendar', function(p){ $('#calendar').fullCalendar('refetchEvents', cal.calendars[p.source]); }); + rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); + rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); }); + + // let's go + var cal = new rcube_calendar_ui(rcmail.env.calendar_settings); + + $(window).resize(function() { + $('#calendar').fullCalendar('option', 'height', $('#main').height()); + }).resize(); + + // show toolbar + $('#toolbar').show(); + +}); diff --git a/plugins/calendar/skins/default/fullcalendar.printl.css b/plugins/calendar/skins/default/fullcalendar.printl.css new file mode 100644 index 00000000..9006672e --- /dev/null +++ b/plugins/calendar/skins/default/fullcalendar.printl.css @@ -0,0 +1,97 @@ +/* + * FullCalendar v1.5.1 Print Stylesheet + * + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + * + * Copyright (c) 2011 Adam Shaw + * Dual licensed under the MIT and GPL licenses, located in + * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. + * + * Date: Sat Apr 9 14:09:51 2011 -0700 + * + */ + + + /* Events +-----------------------------------------------------*/ + +/** +.fc-event-skin { + background: none !important; + color: #000 !important; + } + + + +.fc-event-hori { + border-width: 0 0 1px 0 !important; + border-bottom-style: dotted !important; + border-bottom-color: #000 !important; + padding: 1px 0 0 0 !important; + } + +.fc-event-hori .fc-event-inner { + border-width: 0 !important; + padding: 0 1px !important; + } + + +.fc-event-vert { + border-width: 0 0 0 1px !important; + border-left-style: dotted !important; + border-left-color: #000 !important; + padding: 0 1px 0 0 !important; + } + +.fc-event-vert .fc-event-inner { + border-width: 0 !important; + padding: 1px 0 !important; + } + +.fc-event-bg { + display: none !important; + } + +.fc-event .ui-resizable-handle { + display: none !important; + } + + */ + + + +/** + * UI + */ + +#calendar { + position: absolute; + overflow: visible; + top: 0; + left: 2px; + right: 2px; + bottom: 0; +} + +#calendars { + position: absolute; + display:none; + top: 25px; + left: 0; + bottom: 0; + right: 0; + background-color: #F9F9F9; + border: 1px solid #999999; + overflow: hidden; +} + +#main { + position: absolute; + clear: both; + top: 25px; + left: 0; + right: 0; + bottom: 10px; +} \ No newline at end of file diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index 394258db..2c8d68bc 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -204,9 +204,6 @@
-
- Stub -
+ + + +
+
+
+
+
+ +
+
+
+ +
+ + +
+ + + \ No newline at end of file