443 lines
16 KiB
JavaScript
443 lines
16 KiB
JavaScript
/**
|
|
* Basic Javascript utilities for calendar-related plugins
|
|
*
|
|
* @version @package_version@
|
|
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
|
*
|
|
* Copyright (C) 2012, 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/>.
|
|
*/
|
|
|
|
function rcube_libcalendaring(settings)
|
|
{
|
|
// member vars
|
|
this.settings = settings;
|
|
this.alarm_ids = [];
|
|
this.alarm_dialog = null;
|
|
this.snooze_popup = null;
|
|
this.dismiss_link = null;
|
|
|
|
// 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)
|
|
{
|
|
if (!event.start)
|
|
return '';
|
|
if (!event.end)
|
|
event.end = event.start;
|
|
|
|
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
|
|
if (event.allDay) {
|
|
fromto = this.format_datetime(event.start, 1)
|
|
+ (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' — ' + this.format_datetime(event.end, 1) : '');
|
|
}
|
|
else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
|
|
fromto = this.format_datetime(event.start, 0)
|
|
+ (duration > 0 ? ' — ' + this.format_datetime(event.end, 2) : '');
|
|
}
|
|
else {
|
|
fromto = this.format_datetime(event.start, 0)
|
|
+ (duration > 0 ? ' — ' + this.format_datetime(event.end, 0) : '');
|
|
}
|
|
|
|
return fromto;
|
|
};
|
|
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function parseISO8601(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, 1),
|
|
check = new Date(m[1], 0, 1, 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;
|
|
}
|
|
|
|
/**
|
|
* Format the given date object according to user's prefs
|
|
*/
|
|
this.format_datetime = function(date, mode)
|
|
{
|
|
var res = '';
|
|
if (!mode || mode == 1)
|
|
res += $.datepicker.formatDate(datepicker_settings.dateFormat, date, datepicker_settings);
|
|
if (!mode)
|
|
res += ' ';
|
|
if (!mode || mode == 2)
|
|
res += this.format_time(date);
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Clone from fullcalendar.js
|
|
*/
|
|
this.format_time = function(date)
|
|
{
|
|
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 = 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 = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a><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;
|
|
}
|
|
}
|
|
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');
|
|
|
|
return html
|
|
.replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
|
|
.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)
|
|
{
|
|
// register events on alarm fields
|
|
$(prefix+' select.edit-alarm-type').change(function(){
|
|
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
|
|
});
|
|
$(prefix+' select.edit-alarm-offset').change(function(){
|
|
var mode = $(this).val() == '@' ? 'show' : 'hide';
|
|
$(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
|
|
$(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
|
|
});
|
|
|
|
$(prefix+' .edit-alarm-date').datepicker(datepicker_settings);
|
|
}
|
|
|
|
|
|
/***** 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');
|
|
|
|
this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
|
|
|
|
var actions, adismiss, asnooze, alarm, html, event_ids = [];
|
|
for (var i=0; i < alarms.length; i++) {
|
|
alarm = alarms[i];
|
|
alarm.start = parseISO8601(alarm.start);
|
|
alarm.end = parseISO8601(alarm.end);
|
|
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(){
|
|
me.dismiss_link = $(this);
|
|
me.dismiss_alarm(me.dismiss_link.data('id'), 0);
|
|
});
|
|
asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){
|
|
me.snooze_dropdown($(this));
|
|
e.stopPropagation();
|
|
return false;
|
|
});
|
|
actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
|
|
|
|
$('<div>').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog);
|
|
}
|
|
|
|
var buttons = {};
|
|
buttons[rcmail.gettext('dismissall','libcalendaring')] = function() {
|
|
// submit dismissed event_ids to server
|
|
me.dismiss_alarm(me.alarm_ids.join(','), 0);
|
|
$(this).dialog('close');
|
|
};
|
|
|
|
this.alarm_dialog.appendTo(document.body).dialog({
|
|
modal: false,
|
|
resizable: true,
|
|
closeOnEscape: false,
|
|
dialogClass: 'alarm',
|
|
title: '<span class="ui-icon ui-icon-alarms" style="float:left; margin:0 4px 0 0"></span>' + rcmail.gettext('alarmtitle','libcalendaring'),
|
|
buttons: buttons,
|
|
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_ids = event_ids;
|
|
};
|
|
|
|
/**
|
|
* Show a drop-down menu with a selection of snooze times
|
|
*/
|
|
this.snooze_dropdown = function(link)
|
|
{
|
|
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);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// hide visible popup
|
|
if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
|
|
this.snooze_popup.hide();
|
|
this.dismiss_link = null;
|
|
}
|
|
else { // open popup below the clicked link
|
|
var pos = link.offset();
|
|
pos.top += link.height() + 2;
|
|
this.snooze_popup.data('id', link.data('id')).css({ top:Math.floor(pos.top)+'px', left:Math.floor(pos.left)+'px' }).show();
|
|
this.dismiss_link = link;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dismiss or snooze alarms for the given event
|
|
*/
|
|
this.dismiss_alarm = function(id, snooze)
|
|
{
|
|
$('#alarm-snooze-dropdown').hide();
|
|
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;
|
|
};
|
|
}
|
|
|
|
|
|
// 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) {
|
|
var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
|
|
rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
|
|
});
|