Mercurial > hg > rc1
diff plugins/libcalendaring/libcalendaring.js @ 4:888e774ee983
libcalendar plugin as distributed
author | Charlie Root |
---|---|
date | Sat, 13 Jan 2018 08:57:56 -0500 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/libcalendaring/libcalendaring.js Sat Jan 13 08:57:56 2018 -0500 @@ -0,0 +1,1464 @@ +/** + * Basic Javascript utilities for calendar-related plugins + * + * @author Thomas Bruederli <bruederli@kolabsys.com> + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + */ + +function rcube_libcalendaring(settings) +{ + // member vars + this.settings = settings || {}; + this.alarm_ids = []; + this.alarm_dialog = null; + this.snooze_popup = null; + this.dismiss_link = null; + this.group2expand = {}; + + // abort if env isn't set + if (!settings || !settings.date_format) + return; + + // private vars + var me = this; + var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); + var client_timezone = new Date().getTimezoneOffset(); + + // 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 + }; + + + /** + * Quote html entities + */ + var Q = this.quote_html = function(str) + { + return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); + }; + + /** + * Create a nice human-readable string for the date/time range + */ + this.event_date_text = function(event, voice) + { + if (!event.start) + return ''; + if (!event.end) + event.end = event.start; + + var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000, + until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — '; + if (event.allDay) { + fromto = this.format_datetime(event.start, 1, voice) + + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : ''); + } + else if (duration < 86400 && event.start.getDay() == event.end.getDay()) { + fromto = this.format_datetime(event.start, 0, voice) + + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : ''); + } + else { + fromto = this.format_datetime(event.start, 0, voice) + + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : ''); + } + + return fromto; + }; + + /** + * Checks if the event/task has 'real' attendees, excluding the current user + */ + this.has_attendees = function(event) + { + return !!(event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); + }; + + /** + * Check if the current user is an attendee of this event/task + */ + this.is_attendee = function(event, role, email) + { + var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails; + + for (i=0; event.attendees && i < event.attendees.length; i++) { + if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) { + return event.attendees[i]; + } + } + + return false; + }; + + /** + * Checks if the current user is the organizer of the event/task + */ + this.is_organizer = function(event, email) + { + return this.is_attendee(event, 'ORGANIZER', email) || !event.id; + }; + + /** + * Check permissions on the given folder object + */ + this.has_permission = function(folder, perm) + { + // multiple chars means "either of" + if (String(perm).length > 1) { + for (var i=0; i < perm.length; i++) { + if (this.has_permission(folder, perm[i])) { + return true; + } + } + } + + if (folder.rights && String(folder.rights).indexOf(perm) >= 0) { + return true; + } + + return (perm == 'i' && folder.editable) || (perm == 'v' && folder.editable); + }; + + + /** + * From time and date strings to a real date object + */ + this.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(/p[.m]*/i) && date.getHours() < 12) + date.setHours(parseInt(time_arr[0]) + 12); + else if (time.match(/a[.m]*/i) && date.getHours() == 12) + date.setHours(0); + } + if (!isNaN(time_arr[1])) + date.setMinutes(time_arr[1]); + + return date; + } + + /** + * Convert an ISO 8601 formatted date string from the server into a Date object. + * Timezone information will be ignored, the server already provides dates in user's timezone. + */ + this.parseISO8601 = function(s) + { + // already a Date object? + if (s && s.getMonth) { + return s; + } + + // force d to be on check's YMD, for daylight savings purposes + var fixDate = function(d, check) { + if (+d) { // prevent infinite looping on invalid dates + while (d.getDate() != check.getDate()) { + d.setTime(+d + (d < check ? 1 : -1) * 3600000); + } + } + } + + // derived from http://delete.me.uk/2005/03/iso8601.html + var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); + if (!m) { + return null; + } + + var date = new Date(m[1], 0, 2), + check = new Date(m[1], 0, 2, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + + return date; + } + + /** + * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime() + */ + this.date2ISO8601 = function(date) + { + var zeropad = function(num) { return (num < 10 ? '0' : '') + num; }; + + return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate()) + + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds()); + }; + + /** + * Format the given date object according to user's prefs + */ + this.format_datetime = function(date, mode, voice) + { + var res = ''; + if (!mode || mode == 1) { + res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings); + } + if (!mode) { + res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' '; + } + if (!mode || mode == 2) { + res += this.format_time(date, voice); + } + + return res; + } + + /** + * Clone from fullcalendar.js + */ + this.format_time = function(date, voice) + { + var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; } + var formatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' } + }; + + var i, i2, c, formatter, res = '', + format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format']; + for (i=0; i < format.length; i++) { + c = format.charAt(i); + for (i2=Math.min(i+2, format.length); i2 > i; i2--) { + if (formatter = formatters[format.substring(i, i2)]) { + res += formatter(date); + i = i2 - 1; + break; + } + } + if (i2 == i) { + res += c; + } + } + + return res; + } + + /** + * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings + */ + this.date2unixtime = function(date) + { + var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset + return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset); + } + + /** + * Turn a unix timestamp value into a Date object + */ + this.fromunixtime = function(ts) + { + ts -= gmt_offset * 3600; + var date = new Date(ts * 1000), + dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; + if (dst_offset) // adjust DST offset + date.setTime((ts + 3600) * 1000); + return date; + } + + /** + * Simple plaintext to HTML converter, makig URLs clickable + */ + this.text2html = function(str, maxlen, maxlines) + { + var html = Q(String(str)); + + // limit visible text length + if (maxlen) { + var morelink = '<span>... <a href="#more" onclick="$(this).parent().hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a></span><span style="display:none">', + lines = html.split(/\r?\n/), + words, out = '', len = 0; + + for (var i=0; i < lines.length; i++) { + len += lines[i].length; + if (maxlines && i == maxlines - 1) { + out += lines[i] + '\n' + morelink; + maxlen = html.length * 2; + } + else if (len > maxlen) { + len = out.length; + words = lines[i].split(' '); + for (var j=0; j < words.length; j++) { + len += words[j].length + 1; + out += words[j] + ' '; + if (len > maxlen) { + out += morelink; + maxlen = html.length * 2; + maxlines = 0; + } + } + out += '\n'; + } + else + out += lines[i] + '\n'; + } + + if (maxlen > str.length) + out += '</span>'; + + html = out; + } + + // simple link parser (similar to rcube_string_replacer class in PHP) + var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})'; + var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-'; + var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig'); + var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig'); + var link_replace = function(matches, p1, p2) { + var title = '', text = p2; + if (p2 && p2.length > 55) { + text = p2.substr(0, 45) + '...' + p2.substr(-8); + title = p1 + p2; + } + return '<a href="'+p1+p2+'" class="extlink" target="_blank" title="'+title+'">'+p1+text+'</a>' + }; + + return html + .replace(link_pattern, link_replace) + .replace(mailto_pattern, '<a href="mailto:$1">$1</a>') + .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"') + .replace(/\n/g, "<br/>"); + }; + + this.init_alarms_edit = function(prefix, index) + { + var edit_type = $(prefix+' select.edit-alarm-type'), + dom_id = edit_type.attr('id'); + + // register events on alarm fields + edit_type.change(function(){ + $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); + }); + $(prefix+' select.edit-alarm-offset').change(function(){ + var val = $(this).val(), parent = $(this).parent(); + parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); + parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); + parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show'](); + }); + + $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); + + $(prefix).on('click', 'a.delete-alarm', function(e){ + if ($(this).closest('.edit-alarm-item').siblings().length > 0) { + $(this).closest('.edit-alarm-item').remove(); + } + return false; + }); + + // set a unique id attribute and set label reference accordingly + if ((index || 0) > 0 && dom_id) { + dom_id += ':' + (new Date().getTime()); + edit_type.attr('id', dom_id); + $(prefix+' label:first').attr('for', dom_id); + } + + $(prefix).on('click', 'a.add-alarm', function(e){ + var i = $(this).closest('.edit-alarm-item').siblings().length + 1; + var item = $(this).closest('.edit-alarm-item').clone(false) + .removeClass('first') + .appendTo(prefix); + + me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); + $('select.edit-alarm-type, select.edit-alarm-offset', item).change(); + return false; + }); + } + + this.set_alarms_edit = function(prefix, valarms) + { + $(prefix + ' .edit-alarm-item:gt(0)').remove(); + + var i, alarm, domnode, val, offset; + for (i=0; i < valarms.length; i++) { + alarm = valarms[i]; + if (!alarm.action) + alarm.action = 'DISPLAY'; + + if (i == 0) { + domnode = $(prefix + ' .edit-alarm-item').eq(0); + } + else { + domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix); + this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); + } + + $('select.edit-alarm-type', domnode).val(alarm.action); + $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start'); + + if (String(alarm.trigger).match(/@(\d+)/)) { + var ondate = this.fromunixtime(parseInt(RegExp.$1)); + $('select.edit-alarm-offset', domnode).val('@'); + $('input.edit-alarm-value', domnode).val(''); + $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1)); + $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2)); + } + else if (String(alarm.trigger).match(/^[-+]*0[MHDS]$/)) { + $('input.edit-alarm-value', domnode).val('0'); + $('select.edit-alarm-offset', domnode).val('0'); + } + else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) { + val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3; + $('input.edit-alarm-value', domnode).val(val); + $('select.edit-alarm-offset', domnode).val(offset); + } + } + + // set correct visibility by triggering onchange handlers + $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change(); + }; + + this.serialize_alarms = function(prefix) + { + var valarms = []; + + $(prefix + ' .edit-alarm-item').each(function(i, elem) { + var val, offset, alarm = { + action: $('select.edit-alarm-type', elem).val(), + related: $('select.edit-alarm-related', elem).val() + }; + + if (alarm.action) { + offset = $('select.edit-alarm-offset', elem).val(); + if (offset == '@') { + alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val())); + } + else if (offset === '0') { + alarm.trigger = '0S'; + } + else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) { + alarm.trigger = offset[0] + val + offset[1]; + } + + valarms.push(alarm); + } + }); + + return valarms; + }; + + // format time string + var time_autocomplete_format = function(hour, minutes, start) { + var time, diff, unit, duration = '', d = new Date(); + + d.setHours(hour); + d.setMinutes(minutes); + time = me.format_time(d); + + if (start) { + diff = Math.floor((d.getTime() - start.getTime()) / 60000); + if (diff > 0) { + unit = 'm'; + if (diff >= 60) { + unit = 'h'; + diff = Math.round(diff / 3) / 20; + } + duration = ' (' + diff + unit + ')'; + } + } + + return [time, duration]; + }; + + var time_autocomplete_list = function(p, callback) { + // Time completions + var st, h, step = 15, result = [], now = new Date(), + id = String(this.element.attr('id')), + m = id.match(/^(.*)-(starttime|endtime)$/), + start = (m && m[2] == 'endtime' + && (st = $('#' + m[1] + '-starttime').val()) + && $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val()) + ? me.parse_datetime(st, '') : null, + full = p.term - 1 > 0 || p.term.length > 1, + hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(), + minutes = hours * 60 + (full ? 0 : now.getMinutes()), + min = Math.ceil(minutes / step) * step % 60, + hour = Math.floor(Math.ceil(minutes / step) * step / 60); + + // list hours from 0:00 till now + for (h = start ? start.getHours() : 0; h < hours; h++) + result.push(time_autocomplete_format(h, 0, start)); + + // list 15min steps for the next two hours + for (; h < hour + 2 && h < 24; h++) { + while (min < 60) { + result.push(time_autocomplete_format(h, min, start)); + min += step; + } + min = 0; + } + + // list the remaining hours till 23:00 + while (h < 24) + result.push(time_autocomplete_format((h++), 0, start)); + + return callback(result); + }; + + var time_autocomplete_open = function(event, ui) { + // scroll to current time + var $this = $(this), + widget = $this.autocomplete('widget') + menu = $this.data('ui-autocomplete').menu, + amregex = /^(.+)(a[.m]*)/i, + pmregex = /^(.+)(a[.m]*)/i, + val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1'); + + widget.css('width', '10em'); + + if (val === '') + menu._scrollIntoView(widget.children('li:first')); + else + widget.children().each(function() { + var li = $(this), + html = li.children().first().html() + .replace(/\s+\(.+\)$/, '') + .replace(amregex, '0:$1') + .replace(pmregex, '1:$1'); + + if (html.indexOf(val) == 0) + menu._scrollIntoView(li); + }); + }; + + /** + * Initializes time autocompletion + */ + this.init_time_autocomplete = function(elem, props) + { + var default_props = { + delay: 100, + minLength: 1, + appendTo: props.container, + source: time_autocomplete_list, + open: time_autocomplete_open, + // change: time_autocomplete_change, + select: function(event, ui) { + $(this).val(ui.item[0]).change(); + return false; + } + }; + + $(elem).attr('autocomplete', "off") + .autocomplete($.extend(default_props, props)) + .click(function() { // show drop-down upon clicks + $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); + }); + + $(elem).data('ui-autocomplete')._renderItem = function(ul, item) { + return $('<li>') + .data('ui-autocomplete-item', item) + .append('<a>' + item[0] + item[1] + '</a>') + .appendTo(ul); + }; + }; + + /***** Alarms handling *****/ + + /** + * Display a notification for the given pending alarms + */ + this.display_alarms = function(alarms) + { + // clear old alert first + if (this.alarm_dialog) + this.alarm_dialog.dialog('destroy').remove(); + + var i, actions, adismiss, asnooze, alarm, html, + audio_alarms = [], records = [], event_ids = [], buttons = {}; + + for (i=0; i < alarms.length; i++) { + alarm = alarms[i]; + alarm.start = this.parseISO8601(alarm.start); + alarm.end = this.parseISO8601(alarm.end); + + if (alarm.action == 'AUDIO') { + audio_alarms.push(alarm); + continue; + } + + event_ids.push(alarm.id); + + html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>'; + html += '<div class="event-section">' + Q(alarm.location || '') + '</div>'; + html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>'; + + adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(e){ + me.dismiss_link = $(this); + me.dismiss_alarm(me.dismiss_link.data('id'), 0, e); + }); + asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){ + me.snooze_dropdown($(this), e); + e.stopPropagation(); + return false; + }); + actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id)); + + records.push($('<div>').addClass('alarm-item').html(html).append(actions)); + } + + if (audio_alarms.length) + this.audio_alarms(audio_alarms); + + if (!records.length) + return; + + this.alarm_dialog = $('<div>').attr('id', 'alarm-display').append(records); + + buttons[rcmail.gettext('close')] = function() { + $(this).dialog('close'); + }; + + buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) { + // submit dismissed event_ids to server + me.dismiss_alarm(me.alarm_ids.join(','), 0, e); + $(this).dialog('close'); + }; + + this.alarm_dialog.appendTo(document.body).dialog({ + modal: false, + resizable: true, + closeOnEscape: false, + dialogClass: 'alarms', + title: rcmail.gettext('alarmtitle','libcalendaring'), + buttons: buttons, + open: function() { + setTimeout(function() { + me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); + }, 5); + }, + close: function() { + $('#alarm-snooze-dropdown').hide(); + $(this).dialog('destroy').remove(); + me.alarm_dialog = null; + me.alarm_ids = null; + }, + drag: function(event, ui) { + $('#alarm-snooze-dropdown').hide(); + } + }); + + this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog'); + + this.alarm_ids = event_ids; + }; + + /** + * Display a notification and play a sound for a set of alarms + */ + this.audio_alarms = function(alarms) + { + var elem, txt = [], + src = rcmail.assets_path('plugins/libcalendaring/alarm'), + plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {}; + + // first generate and display notification text + $.each(alarms, function() { txt.push(this.title); }); + + rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000); + + // Internet Explorer does not support wav files, + // support in other browsers depends on enabled plugins, + // so we use wav as a fallback + src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav'; + + // HTML5 + try { + elem = $('<audio>').attr('src', src); + elem.get(0).play(); + } + // old method + catch (e) { + elem = $('<embed id="libcalsound" src="' + src + '" hidden=true autostart=true loop=false />'); + elem.appendTo($('body')); + window.setTimeout("$('#libcalsound').remove()", 10000); + } + }; + + /** + * Show a drop-down menu with a selection of snooze times + */ + this.snooze_dropdown = function(link, event) + { + if (!this.snooze_popup) { + this.snooze_popup = $('#alarm-snooze-dropdown'); + // create popup if not found + if (!this.snooze_popup.length) { + this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body); + this.snooze_popup.html(rcmail.env.snooze_select) + } + $('#alarm-snooze-dropdown a').click(function(e){ + var time = String(this.href).replace(/.+#/, ''); + me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time, e); + return false; + }); + } + + // hide visible popup + if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) { + rcmail.command('menu-close', 'alarm-snooze-dropdown', link.get(0), event); + this.dismiss_link = null; + } + else { // open popup below the clicked link + rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event); + this.snooze_popup.data('id', link.data('id')); + this.dismiss_link = link; + } + }; + + /** + * Dismiss or snooze alarms for the given event + */ + this.dismiss_alarm = function(id, snooze, event) + { + rcmail.command('menu-close', 'alarm-snooze-dropdown', null, event); + rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } }); + + // remove dismissed alarm from list + if (this.dismiss_link) { + this.dismiss_link.closest('div.alarm-item').hide(); + var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; }); + if (new_ids.length) + this.alarm_ids = new_ids; + else + this.alarm_dialog.dialog('close'); + } + + this.dismiss_link = null; + }; + + + /***** Recurrence form handling *****/ + + /** + * Install event handlers on recurrence form elements + */ + this.init_recurrence_edit = function(prefix) + { + // toggle recurrence frequency forms + $('#edit-recurrence-frequency').change(function(e){ + var freq = $(this).val().toLowerCase(); + $('.recurrence-form').hide(); + if (freq) { + $('#recurrence-form-'+freq).show(); + if (freq != 'rdate') + $('#recurrence-form-until').show(); + } + }); + $('#recurrence-form-rdate input.button.add').click(function(e){ + var dt, dv = $('#edit-recurrence-rdate-input').val(); + if (dv && (dt = me.parse_datetime('12:00', dv))) { + me.add_rdate(dt); + me.sort_rdates(); + $('#edit-recurrence-rdate-input').val('') + } + else { + $('#edit-recurrence-rdate-input').select(); + } + }); + $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){ + $(this).closest('li').remove(); + return false; + }); + + $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); + $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); }); + $('#edit-recurrence-rdate-input').datepicker(datepicker_settings); + }; + + /** + * Set recurrence form according to the given event/task record + */ + this.set_recurrence_edit = function(rec) + { + var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(), + interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1), + rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1), + rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : ''); + $('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false); + $('#edit-recurrence-rdates').html(''); + + var weekdays = ['SU','MO','TU','WE','TH','FR','SA'], + rrepeat_id = '#edit-recurrence-repeat-forever'; + if (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count'; + else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until'; + $(rrepeat_id).prop('checked', true); + + if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') { + var wdays = rec.recurrence.BYDAY.split(','); + $('input.edit-recurrence-weekly-byday').val(wdays); + } + if (rec.recurrence && rec.recurrence.BYMONTHDAY) { + $('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(',')); + $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']); + } + if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) { + var byday, section = rec.recurrence.FREQ.toLowerCase(); + if ((byday = String(rec.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 (rec.start) { + $('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]); + } + if (rec.recurrence && rec.recurrence.BYMONTH) { + $('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(',')); + } + else if (rec.start) { + $('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]); + } + if (rec.recurrence && rec.recurrence.RDATE) { + $.each(rec.recurrence.RDATE, function(i,rdate){ + me.add_rdate(me.parseISO8601(rdate)); + }); + } + }; + + /** + * Gather recurrence settings from form + */ + this.serialize_recurrence = function(timestr) + { + var recurrence = '', + freq = $('#edit-recurrence-frequency').val(); + + if (freq != '') { + recurrence = { + FREQ: freq, + INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val() + }; + + var until = $('input.edit-recurrence-until:checked').val(); + if (until == 'count') + recurrence.COUNT = $('#edit-recurrence-repeat-times').val(); + else if (until == 'until') + recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val())); + + if (freq == 'WEEKLY') { + var byday = []; + $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); }); + if (byday.length) + 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) + recurrence.BYMONTHDAY = bymonday.join(','); + } + else + 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) + recurrence.BYMONTH = bymonth.join(','); + if ((byday = $('#edit-recurrence-yearly-byday').val())) + recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday; + } + else if (freq == 'RDATE') { + recurrence = { RDATE:[] }; + // take selected but not yet added date into account + if ($('#edit-recurrence-rdate-input').val() != '') { + $('#recurrence-form-rdate input.button.add').click(); + } + $('#edit-recurrence-rdates li').each(function(i, li){ + recurrence.RDATE.push($(li).attr('data-value')); + }); + } + } + + return recurrence; + }; + + // add the given date to the RDATE list + this.add_rdate = function(date) + { + var li = $('<li>') + .attr('data-value', this.date2ISO8601(date)) + .html('<span>' + Q(this.format_datetime(date, 1)) + '</span>') + .appendTo('#edit-recurrence-rdates'); + + $('<a>').attr('href', '#del') + .addClass('iconbutton delete') + .html(rcmail.get_label('delete', 'libcalendaring')) + .attr('title', rcmail.get_label('delete', 'libcalendaring')) + .appendTo(li); + }; + + // re-sort the list items by their 'data-value' attribute + this.sort_rdates = function() + { + var mylist = $('#edit-recurrence-rdates'), + listitems = mylist.children('li').get(); + listitems.sort(function(a, b) { + var compA = $(a).attr('data-value'); + var compB = $(b).attr('data-value'); + return (compA < compB) ? -1 : (compA > compB) ? 1 : 0; + }) + $.each(listitems, function(idx, item) { mylist.append(item); }); + }; + + + /***** Attendee form handling *****/ + + // expand the given contact group into individual event/task attendees + this.expand_attendee_group = function(e, add, remove) + { + var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'), + role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected'); + + this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove } + + // copy group role from the according form element + if (role_select.length) { + this.group2expand[id].data.role = role_select.val(); + } + + // register callback handler + if (!this._expand_attendee_listener) { + this._expand_attendee_listener = this.expand_attendee_callback; + rcmail.addEventListener('plugin.expand_attendee_callback', function(result) { + me._expand_attendee_listener(result); + }); + } + + rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading')); + }; + + // callback from server to expand an attendee group + this.expand_attendee_callback = function(result) + { + var attendee, id = result.id, + data = this.group2expand[id], + row = $(data.link).closest('tr'); + + // replace group entry with all members returned by the server + if (data && data.adder && result.members && result.members.length) { + for (var i=0; i < result.members.length; i++) { + attendee = result.members[i]; + attendee.role = data.data.role; + attendee.cutype = 'INDIVIDUAL'; + attendee.status = 'NEEDS-ACTION'; + data.adder(attendee, null, row); + } + + if (data.remover) { + data.remover(data.link, id) + } + else { + row.remove(); + } + + delete this.group2expand[id]; + } + else { + rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error'); + } + }; + + + // Render message reference links to the given container + this.render_message_links = function(links, container, edit, plugin) + { + var ul = $('<ul>').addClass('attachmentslist'); + + $.each(links, function(i, link) { + if (!link.mailurl) + return true; // continue + + var li = $('<li>').addClass('link') + .addClass('message eml') + .append($('<a>') + .attr('href', link.mailurl) + .addClass('messagelink') + .text(link.subject || link.uri) + ) + .appendTo(ul); + + // add icon to remove the link + if (edit) { + $('<a>') + .attr('href', '#delete') + .attr('title', rcmail.gettext('removelink', plugin)) + .attr('data-uri', link.uri) + .addClass('delete') + .text(rcmail.gettext('delete')) + .appendTo(li); + } + }); + + container.empty().append(ul); + } + + // resize and reposition (center) the dialog window + this.dialog_resize = function(id, height, width) + { + var win = $(window), w = win.width(), h = win.height(); + + $(id).dialog('option', { + height: Math.min(h-20, height+130), + width: Math.min(w-20, width+50) + }); + }; +} + +////// static methods + +// render HTML code for displaying an attendee record +rcube_libcalendaring.attendee_html = function(data) +{ + var name, tooltip = '', context = 'libcalendaring', + dispname = data.name || data.email, + status = data.role == 'ORGANIZER' ? 'ORGANIZER' : data.status; + + if (status) + status = status.toLowerCase(); + + if (data.email) { + tooltip = data.email; + name = $('<a>').attr({href: 'mailto:' + data.email, 'class': 'mailtolink', 'data-cutype': data.cutype}) + + if (status) + tooltip += ' (' + rcmail.gettext('status' + status, context) + ')'; + } + else { + name = $('<span>'); + } + + if (data['delegated-to']) + tooltip = rcmail.gettext('delegatedto', context) + ' ' + data['delegated-to']; + else if (data['delegated-from']) + tooltip = rcmail.gettext('delegatedfrom', context) + ' ' + data['delegated-from']; + + return $('<span>').append( + $('<span>').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname)) + ).html(); +}; + +/** + * + */ +rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id) +{ + // ask user to delete the declined event from the local calendar (#1670) + var del = false; + if (rcmail.env.rsvp_saved && status == 'declined') { + del = confirm(rcmail.gettext('itip.declinedeleteconfirm')); + } + + // open dialog for iTip delegation + if (status == 'delegated') { + rcube_libcalendaring.itip_delegate_dialog(function(data) { + rcmail.http_post(task + '/itip-delegate', { + _uid: rcmail.env.uid, + _mbox: rcmail.env.mailbox, + _part: mime_id, + _to: data.to, + _rsvp: data.rsvp ? 1 : 0, + _comment: data.comment, + _folder: data.target + }, rcmail.set_busy(true, 'itip.savingdata')); + }, $('#rsvp-'+dom_id+' .folder-select')); + return false; + } + + var noreply = 0, comment = ''; + if (dom_id) { + noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0; + if (!noreply) + comment = $('#reply-comment-'+dom_id).val(); + } + + rcmail.http_post(task + '/mailimportitip', { + _uid: rcmail.env.uid, + _mbox: rcmail.env.mailbox, + _part: mime_id, + _folder: $('#itip-saveto').val(), + _status: status, + _del: del?1:0, + _noreply: noreply, + _comment: comment + }, rcmail.set_busy(true, 'itip.savingdata')); + + return false; +}; + +/** + * Helper function to render the iTip delegation dialog + * and trigger a callback function when submitted. + */ +rcube_libcalendaring.itip_delegate_dialog = function(callback, selector) +{ + // show dialog for entering the delegatee address and comment + var html = '<form class="itip-dialog-form" action="javascript:void()">' + + '<div class="form-section">' + + '<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' + + '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' + + '</div>' + + '<div class="form-section">' + + '<label for="itip-delegate-rsvp">' + + '<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' + + rcmail.gettext('itip.delegatersvpme') + + '</label>' + + '</div>' + + '<div class="form-section">' + + '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' + + rcmail.gettext('itip.itipcomment') + '"></textarea>' + + '</div>' + + '<div class="form-section">' + + (selector && selector.length ? selector.html() : '') + + '</div>' + + '</form>'; + + var dialog, buttons = []; + buttons.push({ + text: rcmail.gettext('itipdelegated', 'itip'), + click: function() { + var doc = window.parent.document, + delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, ''); + + if (delegatee != '' && rcube_check_email(delegatee, true)) { + callback({ + to: delegatee, + rsvp: $('#itip-delegate-rsvp', doc).prop('checked'), + comment: $('#itip-delegate-comment', doc).val(), + target: $('#itip-saveto', doc).val() + }); + + setTimeout(function() { dialog.dialog("close"); }, 500); + } + else { + alert(rcmail.gettext('itip.delegateinvalidaddress')); + $('#itip-delegate-to', doc).focus(); + } + } + }); + + buttons.push({ + text: rcmail.gettext('cancel', 'itip'), + click: function() { + dialog.dialog('close'); + } + }); + + dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, { + width: 460, + open: function(event, ui) { + $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction'); + $(this).find('#itip-saveto').val(''); + + // initialize autocompletion + var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail; + if (rcmail.env.autocomplete_threads > 0) { + ac_props = { + threads: rcmail.env.autocomplete_threads, + sources: rcmail.env.autocomplete_sources + }; + } + rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props); + rcm.env.recipients_delimiter = ''; + }, + close: function(event, ui) { + rcm = rcmail.is_framed() ? parent.rcmail : rcmail; + rcm.ksearch_blur(); + $(this).remove(); + } + }); + + return dialog; +}; + +/** + * Show a menu for selecting the RSVP reply mode + */ +rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback) +{ + var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode'); + + $.each(['all','current'/*,'future'*/], function(i, mode) { + $('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>') + .addClass('ui-menu-item') + .attr('rel', mode) + .appendTo(mnu); + }); + + var action = btn.attr('rel'); + + // open the mennu + mnu.menu({ + select: function(event, ui) { + callback(action, ui.item.attr('rel')); + } + }) + .appendTo(document.body) + .position({ my: 'left top', at: 'left bottom+2', of: btn }) + .data('action', action); + + setTimeout(function() { + $(document).one('click', function() { + mnu.menu('destroy'); + mnu.remove(); + }); + }, 100); +}; + +/** + * + */ +rcube_libcalendaring.remove_from_itip = function(event, task, title) +{ + if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) { + rcmail.http_post(task + '/itip-remove', + event, + rcmail.set_busy(true, 'itip.savingdata') + ); + } +}; + +/** + * + */ +rcube_libcalendaring.decline_attendee_reply = function(mime_id, task) +{ + // show dialog for entering a comment and send to server + var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' + + '<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>'; + + var dialog, buttons = []; + buttons.push({ + text: rcmail.gettext('declineattendee', 'itip'), + click: function() { + rcmail.http_post(task + '/itip-decline-reply', { + _uid: rcmail.env.uid, + _mbox: rcmail.env.mailbox, + _part: mime_id, + _comment: $('#itip-decline-comment', window.parent.document).val() + }, rcmail.set_busy(true, 'itip.savingdata')); + dialog.dialog("close"); + } + }); + + buttons.push({ + text: rcmail.gettext('cancel', 'itip'), + click: function() { + dialog.dialog('close'); + } + }); + + dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, { + width: 460, + open: function() { + $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction'); + $('#itip-decline-comment').focus(); + } + }); + + return false; +}; + +/** + * + */ +rcube_libcalendaring.fetch_itip_object_status = function(p) +{ + rcmail.http_post(p.task + '/itip-status', { data: p }); +}; + +/** + * + */ +rcube_libcalendaring.update_itip_object_status = function(p) +{ + rcmail.env.rsvp_saved = p.saved; + rcmail.env.itip_existing = p.existing; + + // hide all elements first + $('#itip-buttons-'+p.id+' > div').hide(); + $('#rsvp-'+p.id+' .folder-select').remove(); + + if (p.html) { + // append/replace rsvp status display + $('#loading-'+p.id).next('.rsvp-status').remove(); + $('#loading-'+p.id).hide().after(p.html); + } + + // enable/disable rsvp buttons + if (p.action == 'rsvp') { + $('#rsvp-'+p.id+' input.button').prop('disabled', false) + .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest); + } + + // show rsvp/import buttons (with calendar selector) + $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select); + + // highlight date if date change detected + if (p.resheduled) + $('.calendar-eventdetails td.date').addClass('modified'); + + // show itip box appendix after replacing the given placeholders + if (p.append && p.append.selector) { + var elem = $(p.append.selector); + if (p.append.replacements) { + $.each(p.append.replacements, function(k, html) { + elem.html(elem.html().replace(k, html)); + }); + } + else if (p.append.html) { + elem.html(p.append.html) + } + elem.show(); + } +}; + +/** + * Callback from server after an iTip message has been processed + */ +rcube_libcalendaring.itip_message_processed = function(metadata) +{ + if (metadata.after_action) { + setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200); + } + else { + rcube_libcalendaring.fetch_itip_object_status(metadata); + } +}; + +/** + * After-action on iTip request message. Action types: + * 0 - no action + * 1 - move to Trash + * 2 - delete the message + * 3 - flag as deleted + * folder_name - move the message to the specified folder + */ +rcube_libcalendaring.itip_after_action = function(action) +{ + if (!action) { + return; + } + + var rc = rcmail.is_framed() ? parent.rcmail : rcmail; + + if (action === 2) { + rc.permanently_remove_messages(); + } + else if (action === 3) { + rc.mark_message('delete'); + } + else { + rc.move_messages(action === 1 ? rc.env.trash_mailbox : action); + } +}; + +/** + * Open the calendar preview for the current iTip event + */ +rcube_libcalendaring.open_itip_preview = function(url, msgref) +{ + if (!rcmail.env.itip_existing) + url += '&itip=' + escape(msgref); + + var win = rcmail.open_window(url); +}; + + +// extend jQuery +(function($){ + $.fn.serializeJSON = function(){ + var json = {}; + jQuery.map($(this).serializeArray(), function(n, i) { + json[n['name']] = n['value']; + }); + return json; + }; +})(jQuery); + + +/* libcalendaring plugin initialization */ +window.rcmail && rcmail.addEventListener('init', function(evt) { + if (rcmail.env.libcal_settings) { + var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings); + rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); }); + } + + rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status) + .addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status) + .addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed); + + if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects['attachmentframe']) { + rcmail.register_command('print-attachment', function() { + var frame = rcmail.get_frame_window(rcmail.gui_objects['attachmentframe'].id); + if (frame) frame.print(); + }, true); + } + + if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) { + rcmail.register_command('download-attachment', function() { + rcmail.location_href(rcmail.env.attachment_download_url, window); + }, true); + } +});