/** * 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 ? settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y') : 'yy-mm-dd', 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 && !event.allDayfake) { 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. */ this.parseISO8601 = function(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; } /** * 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) { 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) { if (parent.tabbed && !self.tabbed) { // Mod by Rosali (tabbed plugin compatibility) return; } var focused = $(':focus'); var is_minimized = false; // clear old alert first if (this.alarm_dialog) { // Begin Mod by Rosali (remove previously added divs) this.alarm_dialog.dialog('destroy').remove(); if($('#dialog-extend-fixed-container').get(0)){ is_minimized = true; } $('#dialog-extend-fixed-container').remove(); // End Mod by Rosali } 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 = this.parseISO8601(alarm.start); alarm.end = this.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'); }; // Begin Mod by Rosali // dialog-extend options var dialogExtendOptions = { 'closable' : true, 'maximizable' : false, 'minimizable' : true, 'minimizeLocation' : 'right', 'collapsable' : false, 'dblclick' : 'maximize', 'load' : function(evt, dlg) { if ((rcmail.task == 'mail' && (rcmail.env.action == 'compose' || rcmail.env.action == 'show')) || is_minimized == true) { $('#alarm-display').dialogExtend('minimize'); $('#dialog-extend-fixed-container').children().width(300); if (focused.is('input') || focused.is('textarea')) { $(focused).focus(); } else if (typeof tinymce == 'object') { window.setTimeout("tinymce.get(rcmail.env.composebody).getBody().focus();", 100); } } }, 'minimize' : function (evt, dlg) { $('#dialog-extend-fixed-container').children().width(300); }, }; // End Mod by Rosali 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(); } }).dialogExtend(dialogExtendOptions); // Mod by Rosali 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; }; /***** 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 = $('
  • ') .attr('data-value', this.date2ISO8601(date)) .html('' + Q(this.format_datetime(date, 1)) + '') .appendTo('#edit-recurrence-rdates'); $('').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); }); }; } // 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); }); if (!rcmail.env.framed && !rcmail.env.extwin && rcmail.env.username) { window.setTimeout("rcmail.http_post('refresh', '');", 1000); // Mod by Rosali (fetch reminders almost immediately) } } });