/** * Basic Javascript utilities for calendar-related plugins * * @version @package_version@ * @author Thomas Bruederli * * Copyright (C) 2012, Kolab Systems AG * * 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 . */ 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, '"'); }; /** * 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 = ' '+rcmail.gettext('showmore','libcalendaring')+'', 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 += ''; 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, '$1$2') .replace(mailto_pattern, '$1') .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"') .replace(/\n/g, "
"); }; 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 = $('
').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 = '

' + Q(alarm.title) + '

'; html += '
' + Q(alarm.location || '') + '
'; html += '
' + Q(this.event_date_text(alarm)) + '
'; adismiss = $('').html(rcmail.gettext('dismiss','libcalendaring')).click(function(){ me.dismiss_link = $(this); me.dismiss_alarm(me.dismiss_link.data('id'), 0); }); asnooze = $('').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){ me.snooze_dropdown($(this)); e.stopPropagation(); return false; }); actions = $('
').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id)); $('
').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: '' + 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 = $('
').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); }); });