We're detecting if something changed in description, title, url, location or attendees. These are properties that could be changed by the organizer without SEQUENCE bump. If that is detected we display "Update in my calendar" button. TODO: display a diff of changes, so user can take a decission if he want's to overwrite his copy of the event.
1466 lines
52 KiB
JavaScript
1466 lines
52 KiB
JavaScript
/**
|
|
* 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('libcalendaring.delegatedto') + ' ' + data['delegated-to'];
|
|
else if (data['delegated-from'])
|
|
tooltip = rcmail.gettext('libcalendaring.delegatedfrom') + ' ' + 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)
|
|
{
|
|
p.mbox = rcmail.env.mailbox;
|
|
p.message_uid = rcmail.env.uid;
|
|
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.rescheduled)
|
|
$('.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);
|
|
}
|
|
});
|