Mercurial > hg > rc1
comparison plugins/libcalendaring/libcalendaring.js @ 4:888e774ee983
libcalendar plugin as distributed
author | Charlie Root |
---|---|
date | Sat, 13 Jan 2018 08:57:56 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
3:f6fe4b6ae66a | 4:888e774ee983 |
---|---|
1 /** | |
2 * Basic Javascript utilities for calendar-related plugins | |
3 * | |
4 * @author Thomas Bruederli <bruederli@kolabsys.com> | |
5 * | |
6 * @licstart The following is the entire license notice for the | |
7 * JavaScript code in this page. | |
8 * | |
9 * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com> | |
10 * | |
11 * This program is free software: you can redistribute it and/or modify | |
12 * it under the terms of the GNU Affero General Public License as | |
13 * published by the Free Software Foundation, either version 3 of the | |
14 * License, or (at your option) any later version. | |
15 * | |
16 * This program is distributed in the hope that it will be useful, | |
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 * GNU Affero General Public License for more details. | |
20 * | |
21 * You should have received a copy of the GNU Affero General Public License | |
22 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
23 * | |
24 * @licend The above is the entire license notice | |
25 * for the JavaScript code in this page. | |
26 */ | |
27 | |
28 function rcube_libcalendaring(settings) | |
29 { | |
30 // member vars | |
31 this.settings = settings || {}; | |
32 this.alarm_ids = []; | |
33 this.alarm_dialog = null; | |
34 this.snooze_popup = null; | |
35 this.dismiss_link = null; | |
36 this.group2expand = {}; | |
37 | |
38 // abort if env isn't set | |
39 if (!settings || !settings.date_format) | |
40 return; | |
41 | |
42 // private vars | |
43 var me = this; | |
44 var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); | |
45 var client_timezone = new Date().getTimezoneOffset(); | |
46 | |
47 // general datepicker settings | |
48 var datepicker_settings = { | |
49 // translate from fullcalendar format to datepicker format | |
50 dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'), | |
51 firstDay : settings.first_day, | |
52 dayNamesMin: settings.days_short, | |
53 monthNames: settings.months, | |
54 monthNamesShort: settings.months, | |
55 changeMonth: false, | |
56 showOtherMonths: true, | |
57 selectOtherMonths: true | |
58 }; | |
59 | |
60 | |
61 /** | |
62 * Quote html entities | |
63 */ | |
64 var Q = this.quote_html = function(str) | |
65 { | |
66 return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
67 }; | |
68 | |
69 /** | |
70 * Create a nice human-readable string for the date/time range | |
71 */ | |
72 this.event_date_text = function(event, voice) | |
73 { | |
74 if (!event.start) | |
75 return ''; | |
76 if (!event.end) | |
77 event.end = event.start; | |
78 | |
79 var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000, | |
80 until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — '; | |
81 if (event.allDay) { | |
82 fromto = this.format_datetime(event.start, 1, voice) | |
83 + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : ''); | |
84 } | |
85 else if (duration < 86400 && event.start.getDay() == event.end.getDay()) { | |
86 fromto = this.format_datetime(event.start, 0, voice) | |
87 + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : ''); | |
88 } | |
89 else { | |
90 fromto = this.format_datetime(event.start, 0, voice) | |
91 + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : ''); | |
92 } | |
93 | |
94 return fromto; | |
95 }; | |
96 | |
97 /** | |
98 * Checks if the event/task has 'real' attendees, excluding the current user | |
99 */ | |
100 this.has_attendees = function(event) | |
101 { | |
102 return !!(event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); | |
103 }; | |
104 | |
105 /** | |
106 * Check if the current user is an attendee of this event/task | |
107 */ | |
108 this.is_attendee = function(event, role, email) | |
109 { | |
110 var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails; | |
111 | |
112 for (i=0; event.attendees && i < event.attendees.length; i++) { | |
113 if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) { | |
114 return event.attendees[i]; | |
115 } | |
116 } | |
117 | |
118 return false; | |
119 }; | |
120 | |
121 /** | |
122 * Checks if the current user is the organizer of the event/task | |
123 */ | |
124 this.is_organizer = function(event, email) | |
125 { | |
126 return this.is_attendee(event, 'ORGANIZER', email) || !event.id; | |
127 }; | |
128 | |
129 /** | |
130 * Check permissions on the given folder object | |
131 */ | |
132 this.has_permission = function(folder, perm) | |
133 { | |
134 // multiple chars means "either of" | |
135 if (String(perm).length > 1) { | |
136 for (var i=0; i < perm.length; i++) { | |
137 if (this.has_permission(folder, perm[i])) { | |
138 return true; | |
139 } | |
140 } | |
141 } | |
142 | |
143 if (folder.rights && String(folder.rights).indexOf(perm) >= 0) { | |
144 return true; | |
145 } | |
146 | |
147 return (perm == 'i' && folder.editable) || (perm == 'v' && folder.editable); | |
148 }; | |
149 | |
150 | |
151 /** | |
152 * From time and date strings to a real date object | |
153 */ | |
154 this.parse_datetime = function(time, date) | |
155 { | |
156 // we use the utility function from datepicker to parse dates | |
157 var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date(); | |
158 | |
159 var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/); | |
160 if (!isNaN(time_arr[0])) { | |
161 date.setHours(time_arr[0]); | |
162 if (time.match(/p[.m]*/i) && date.getHours() < 12) | |
163 date.setHours(parseInt(time_arr[0]) + 12); | |
164 else if (time.match(/a[.m]*/i) && date.getHours() == 12) | |
165 date.setHours(0); | |
166 } | |
167 if (!isNaN(time_arr[1])) | |
168 date.setMinutes(time_arr[1]); | |
169 | |
170 return date; | |
171 } | |
172 | |
173 /** | |
174 * Convert an ISO 8601 formatted date string from the server into a Date object. | |
175 * Timezone information will be ignored, the server already provides dates in user's timezone. | |
176 */ | |
177 this.parseISO8601 = function(s) | |
178 { | |
179 // already a Date object? | |
180 if (s && s.getMonth) { | |
181 return s; | |
182 } | |
183 | |
184 // force d to be on check's YMD, for daylight savings purposes | |
185 var fixDate = function(d, check) { | |
186 if (+d) { // prevent infinite looping on invalid dates | |
187 while (d.getDate() != check.getDate()) { | |
188 d.setTime(+d + (d < check ? 1 : -1) * 3600000); | |
189 } | |
190 } | |
191 } | |
192 | |
193 // derived from http://delete.me.uk/2005/03/iso8601.html | |
194 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}))?))?)?)?)?$/); | |
195 if (!m) { | |
196 return null; | |
197 } | |
198 | |
199 var date = new Date(m[1], 0, 2), | |
200 check = new Date(m[1], 0, 2, 9, 0); | |
201 if (m[3]) { | |
202 date.setMonth(m[3] - 1); | |
203 check.setMonth(m[3] - 1); | |
204 } | |
205 if (m[5]) { | |
206 date.setDate(m[5]); | |
207 check.setDate(m[5]); | |
208 } | |
209 fixDate(date, check); | |
210 if (m[7]) { | |
211 date.setHours(m[7]); | |
212 } | |
213 if (m[8]) { | |
214 date.setMinutes(m[8]); | |
215 } | |
216 if (m[10]) { | |
217 date.setSeconds(m[10]); | |
218 } | |
219 if (m[12]) { | |
220 date.setMilliseconds(Number("0." + m[12]) * 1000); | |
221 } | |
222 fixDate(date, check); | |
223 | |
224 return date; | |
225 } | |
226 | |
227 /** | |
228 * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime() | |
229 */ | |
230 this.date2ISO8601 = function(date) | |
231 { | |
232 var zeropad = function(num) { return (num < 10 ? '0' : '') + num; }; | |
233 | |
234 return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate()) | |
235 + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds()); | |
236 }; | |
237 | |
238 /** | |
239 * Format the given date object according to user's prefs | |
240 */ | |
241 this.format_datetime = function(date, mode, voice) | |
242 { | |
243 var res = ''; | |
244 if (!mode || mode == 1) { | |
245 res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings); | |
246 } | |
247 if (!mode) { | |
248 res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' '; | |
249 } | |
250 if (!mode || mode == 2) { | |
251 res += this.format_time(date, voice); | |
252 } | |
253 | |
254 return res; | |
255 } | |
256 | |
257 /** | |
258 * Clone from fullcalendar.js | |
259 */ | |
260 this.format_time = function(date, voice) | |
261 { | |
262 var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; } | |
263 var formatters = { | |
264 s : function(d) { return d.getSeconds() }, | |
265 ss : function(d) { return zeroPad(d.getSeconds()) }, | |
266 m : function(d) { return d.getMinutes() }, | |
267 mm : function(d) { return zeroPad(d.getMinutes()) }, | |
268 h : function(d) { return d.getHours() % 12 || 12 }, | |
269 hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, | |
270 H : function(d) { return d.getHours() }, | |
271 HH : function(d) { return zeroPad(d.getHours()) }, | |
272 t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, | |
273 tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, | |
274 T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, | |
275 TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' } | |
276 }; | |
277 | |
278 var i, i2, c, formatter, res = '', | |
279 format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format']; | |
280 for (i=0; i < format.length; i++) { | |
281 c = format.charAt(i); | |
282 for (i2=Math.min(i+2, format.length); i2 > i; i2--) { | |
283 if (formatter = formatters[format.substring(i, i2)]) { | |
284 res += formatter(date); | |
285 i = i2 - 1; | |
286 break; | |
287 } | |
288 } | |
289 if (i2 == i) { | |
290 res += c; | |
291 } | |
292 } | |
293 | |
294 return res; | |
295 } | |
296 | |
297 /** | |
298 * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings | |
299 */ | |
300 this.date2unixtime = function(date) | |
301 { | |
302 var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset | |
303 return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset); | |
304 } | |
305 | |
306 /** | |
307 * Turn a unix timestamp value into a Date object | |
308 */ | |
309 this.fromunixtime = function(ts) | |
310 { | |
311 ts -= gmt_offset * 3600; | |
312 var date = new Date(ts * 1000), | |
313 dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; | |
314 if (dst_offset) // adjust DST offset | |
315 date.setTime((ts + 3600) * 1000); | |
316 return date; | |
317 } | |
318 | |
319 /** | |
320 * Simple plaintext to HTML converter, makig URLs clickable | |
321 */ | |
322 this.text2html = function(str, maxlen, maxlines) | |
323 { | |
324 var html = Q(String(str)); | |
325 | |
326 // limit visible text length | |
327 if (maxlen) { | |
328 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">', | |
329 lines = html.split(/\r?\n/), | |
330 words, out = '', len = 0; | |
331 | |
332 for (var i=0; i < lines.length; i++) { | |
333 len += lines[i].length; | |
334 if (maxlines && i == maxlines - 1) { | |
335 out += lines[i] + '\n' + morelink; | |
336 maxlen = html.length * 2; | |
337 } | |
338 else if (len > maxlen) { | |
339 len = out.length; | |
340 words = lines[i].split(' '); | |
341 for (var j=0; j < words.length; j++) { | |
342 len += words[j].length + 1; | |
343 out += words[j] + ' '; | |
344 if (len > maxlen) { | |
345 out += morelink; | |
346 maxlen = html.length * 2; | |
347 maxlines = 0; | |
348 } | |
349 } | |
350 out += '\n'; | |
351 } | |
352 else | |
353 out += lines[i] + '\n'; | |
354 } | |
355 | |
356 if (maxlen > str.length) | |
357 out += '</span>'; | |
358 | |
359 html = out; | |
360 } | |
361 | |
362 // simple link parser (similar to rcube_string_replacer class in PHP) | |
363 var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})'; | |
364 var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-'; | |
365 var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig'); | |
366 var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig'); | |
367 var link_replace = function(matches, p1, p2) { | |
368 var title = '', text = p2; | |
369 if (p2 && p2.length > 55) { | |
370 text = p2.substr(0, 45) + '...' + p2.substr(-8); | |
371 title = p1 + p2; | |
372 } | |
373 return '<a href="'+p1+p2+'" class="extlink" target="_blank" title="'+title+'">'+p1+text+'</a>' | |
374 }; | |
375 | |
376 return html | |
377 .replace(link_pattern, link_replace) | |
378 .replace(mailto_pattern, '<a href="mailto:$1">$1</a>') | |
379 .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"') | |
380 .replace(/\n/g, "<br/>"); | |
381 }; | |
382 | |
383 this.init_alarms_edit = function(prefix, index) | |
384 { | |
385 var edit_type = $(prefix+' select.edit-alarm-type'), | |
386 dom_id = edit_type.attr('id'); | |
387 | |
388 // register events on alarm fields | |
389 edit_type.change(function(){ | |
390 $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); | |
391 }); | |
392 $(prefix+' select.edit-alarm-offset').change(function(){ | |
393 var val = $(this).val(), parent = $(this).parent(); | |
394 parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); | |
395 parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); | |
396 parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show'](); | |
397 }); | |
398 | |
399 $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); | |
400 | |
401 $(prefix).on('click', 'a.delete-alarm', function(e){ | |
402 if ($(this).closest('.edit-alarm-item').siblings().length > 0) { | |
403 $(this).closest('.edit-alarm-item').remove(); | |
404 } | |
405 return false; | |
406 }); | |
407 | |
408 // set a unique id attribute and set label reference accordingly | |
409 if ((index || 0) > 0 && dom_id) { | |
410 dom_id += ':' + (new Date().getTime()); | |
411 edit_type.attr('id', dom_id); | |
412 $(prefix+' label:first').attr('for', dom_id); | |
413 } | |
414 | |
415 $(prefix).on('click', 'a.add-alarm', function(e){ | |
416 var i = $(this).closest('.edit-alarm-item').siblings().length + 1; | |
417 var item = $(this).closest('.edit-alarm-item').clone(false) | |
418 .removeClass('first') | |
419 .appendTo(prefix); | |
420 | |
421 me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); | |
422 $('select.edit-alarm-type, select.edit-alarm-offset', item).change(); | |
423 return false; | |
424 }); | |
425 } | |
426 | |
427 this.set_alarms_edit = function(prefix, valarms) | |
428 { | |
429 $(prefix + ' .edit-alarm-item:gt(0)').remove(); | |
430 | |
431 var i, alarm, domnode, val, offset; | |
432 for (i=0; i < valarms.length; i++) { | |
433 alarm = valarms[i]; | |
434 if (!alarm.action) | |
435 alarm.action = 'DISPLAY'; | |
436 | |
437 if (i == 0) { | |
438 domnode = $(prefix + ' .edit-alarm-item').eq(0); | |
439 } | |
440 else { | |
441 domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix); | |
442 this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); | |
443 } | |
444 | |
445 $('select.edit-alarm-type', domnode).val(alarm.action); | |
446 $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start'); | |
447 | |
448 if (String(alarm.trigger).match(/@(\d+)/)) { | |
449 var ondate = this.fromunixtime(parseInt(RegExp.$1)); | |
450 $('select.edit-alarm-offset', domnode).val('@'); | |
451 $('input.edit-alarm-value', domnode).val(''); | |
452 $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1)); | |
453 $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2)); | |
454 } | |
455 else if (String(alarm.trigger).match(/^[-+]*0[MHDS]$/)) { | |
456 $('input.edit-alarm-value', domnode).val('0'); | |
457 $('select.edit-alarm-offset', domnode).val('0'); | |
458 } | |
459 else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) { | |
460 val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3; | |
461 $('input.edit-alarm-value', domnode).val(val); | |
462 $('select.edit-alarm-offset', domnode).val(offset); | |
463 } | |
464 } | |
465 | |
466 // set correct visibility by triggering onchange handlers | |
467 $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change(); | |
468 }; | |
469 | |
470 this.serialize_alarms = function(prefix) | |
471 { | |
472 var valarms = []; | |
473 | |
474 $(prefix + ' .edit-alarm-item').each(function(i, elem) { | |
475 var val, offset, alarm = { | |
476 action: $('select.edit-alarm-type', elem).val(), | |
477 related: $('select.edit-alarm-related', elem).val() | |
478 }; | |
479 | |
480 if (alarm.action) { | |
481 offset = $('select.edit-alarm-offset', elem).val(); | |
482 if (offset == '@') { | |
483 alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val())); | |
484 } | |
485 else if (offset === '0') { | |
486 alarm.trigger = '0S'; | |
487 } | |
488 else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) { | |
489 alarm.trigger = offset[0] + val + offset[1]; | |
490 } | |
491 | |
492 valarms.push(alarm); | |
493 } | |
494 }); | |
495 | |
496 return valarms; | |
497 }; | |
498 | |
499 // format time string | |
500 var time_autocomplete_format = function(hour, minutes, start) { | |
501 var time, diff, unit, duration = '', d = new Date(); | |
502 | |
503 d.setHours(hour); | |
504 d.setMinutes(minutes); | |
505 time = me.format_time(d); | |
506 | |
507 if (start) { | |
508 diff = Math.floor((d.getTime() - start.getTime()) / 60000); | |
509 if (diff > 0) { | |
510 unit = 'm'; | |
511 if (diff >= 60) { | |
512 unit = 'h'; | |
513 diff = Math.round(diff / 3) / 20; | |
514 } | |
515 duration = ' (' + diff + unit + ')'; | |
516 } | |
517 } | |
518 | |
519 return [time, duration]; | |
520 }; | |
521 | |
522 var time_autocomplete_list = function(p, callback) { | |
523 // Time completions | |
524 var st, h, step = 15, result = [], now = new Date(), | |
525 id = String(this.element.attr('id')), | |
526 m = id.match(/^(.*)-(starttime|endtime)$/), | |
527 start = (m && m[2] == 'endtime' | |
528 && (st = $('#' + m[1] + '-starttime').val()) | |
529 && $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val()) | |
530 ? me.parse_datetime(st, '') : null, | |
531 full = p.term - 1 > 0 || p.term.length > 1, | |
532 hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(), | |
533 minutes = hours * 60 + (full ? 0 : now.getMinutes()), | |
534 min = Math.ceil(minutes / step) * step % 60, | |
535 hour = Math.floor(Math.ceil(minutes / step) * step / 60); | |
536 | |
537 // list hours from 0:00 till now | |
538 for (h = start ? start.getHours() : 0; h < hours; h++) | |
539 result.push(time_autocomplete_format(h, 0, start)); | |
540 | |
541 // list 15min steps for the next two hours | |
542 for (; h < hour + 2 && h < 24; h++) { | |
543 while (min < 60) { | |
544 result.push(time_autocomplete_format(h, min, start)); | |
545 min += step; | |
546 } | |
547 min = 0; | |
548 } | |
549 | |
550 // list the remaining hours till 23:00 | |
551 while (h < 24) | |
552 result.push(time_autocomplete_format((h++), 0, start)); | |
553 | |
554 return callback(result); | |
555 }; | |
556 | |
557 var time_autocomplete_open = function(event, ui) { | |
558 // scroll to current time | |
559 var $this = $(this), | |
560 widget = $this.autocomplete('widget') | |
561 menu = $this.data('ui-autocomplete').menu, | |
562 amregex = /^(.+)(a[.m]*)/i, | |
563 pmregex = /^(.+)(a[.m]*)/i, | |
564 val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1'); | |
565 | |
566 widget.css('width', '10em'); | |
567 | |
568 if (val === '') | |
569 menu._scrollIntoView(widget.children('li:first')); | |
570 else | |
571 widget.children().each(function() { | |
572 var li = $(this), | |
573 html = li.children().first().html() | |
574 .replace(/\s+\(.+\)$/, '') | |
575 .replace(amregex, '0:$1') | |
576 .replace(pmregex, '1:$1'); | |
577 | |
578 if (html.indexOf(val) == 0) | |
579 menu._scrollIntoView(li); | |
580 }); | |
581 }; | |
582 | |
583 /** | |
584 * Initializes time autocompletion | |
585 */ | |
586 this.init_time_autocomplete = function(elem, props) | |
587 { | |
588 var default_props = { | |
589 delay: 100, | |
590 minLength: 1, | |
591 appendTo: props.container, | |
592 source: time_autocomplete_list, | |
593 open: time_autocomplete_open, | |
594 // change: time_autocomplete_change, | |
595 select: function(event, ui) { | |
596 $(this).val(ui.item[0]).change(); | |
597 return false; | |
598 } | |
599 }; | |
600 | |
601 $(elem).attr('autocomplete', "off") | |
602 .autocomplete($.extend(default_props, props)) | |
603 .click(function() { // show drop-down upon clicks | |
604 $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); | |
605 }); | |
606 | |
607 $(elem).data('ui-autocomplete')._renderItem = function(ul, item) { | |
608 return $('<li>') | |
609 .data('ui-autocomplete-item', item) | |
610 .append('<a>' + item[0] + item[1] + '</a>') | |
611 .appendTo(ul); | |
612 }; | |
613 }; | |
614 | |
615 /***** Alarms handling *****/ | |
616 | |
617 /** | |
618 * Display a notification for the given pending alarms | |
619 */ | |
620 this.display_alarms = function(alarms) | |
621 { | |
622 // clear old alert first | |
623 if (this.alarm_dialog) | |
624 this.alarm_dialog.dialog('destroy').remove(); | |
625 | |
626 var i, actions, adismiss, asnooze, alarm, html, | |
627 audio_alarms = [], records = [], event_ids = [], buttons = {}; | |
628 | |
629 for (i=0; i < alarms.length; i++) { | |
630 alarm = alarms[i]; | |
631 alarm.start = this.parseISO8601(alarm.start); | |
632 alarm.end = this.parseISO8601(alarm.end); | |
633 | |
634 if (alarm.action == 'AUDIO') { | |
635 audio_alarms.push(alarm); | |
636 continue; | |
637 } | |
638 | |
639 event_ids.push(alarm.id); | |
640 | |
641 html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>'; | |
642 html += '<div class="event-section">' + Q(alarm.location || '') + '</div>'; | |
643 html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>'; | |
644 | |
645 adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(e){ | |
646 me.dismiss_link = $(this); | |
647 me.dismiss_alarm(me.dismiss_link.data('id'), 0, e); | |
648 }); | |
649 asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){ | |
650 me.snooze_dropdown($(this), e); | |
651 e.stopPropagation(); | |
652 return false; | |
653 }); | |
654 actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id)); | |
655 | |
656 records.push($('<div>').addClass('alarm-item').html(html).append(actions)); | |
657 } | |
658 | |
659 if (audio_alarms.length) | |
660 this.audio_alarms(audio_alarms); | |
661 | |
662 if (!records.length) | |
663 return; | |
664 | |
665 this.alarm_dialog = $('<div>').attr('id', 'alarm-display').append(records); | |
666 | |
667 buttons[rcmail.gettext('close')] = function() { | |
668 $(this).dialog('close'); | |
669 }; | |
670 | |
671 buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) { | |
672 // submit dismissed event_ids to server | |
673 me.dismiss_alarm(me.alarm_ids.join(','), 0, e); | |
674 $(this).dialog('close'); | |
675 }; | |
676 | |
677 this.alarm_dialog.appendTo(document.body).dialog({ | |
678 modal: false, | |
679 resizable: true, | |
680 closeOnEscape: false, | |
681 dialogClass: 'alarms', | |
682 title: rcmail.gettext('alarmtitle','libcalendaring'), | |
683 buttons: buttons, | |
684 open: function() { | |
685 setTimeout(function() { | |
686 me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); | |
687 }, 5); | |
688 }, | |
689 close: function() { | |
690 $('#alarm-snooze-dropdown').hide(); | |
691 $(this).dialog('destroy').remove(); | |
692 me.alarm_dialog = null; | |
693 me.alarm_ids = null; | |
694 }, | |
695 drag: function(event, ui) { | |
696 $('#alarm-snooze-dropdown').hide(); | |
697 } | |
698 }); | |
699 | |
700 this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog'); | |
701 | |
702 this.alarm_ids = event_ids; | |
703 }; | |
704 | |
705 /** | |
706 * Display a notification and play a sound for a set of alarms | |
707 */ | |
708 this.audio_alarms = function(alarms) | |
709 { | |
710 var elem, txt = [], | |
711 src = rcmail.assets_path('plugins/libcalendaring/alarm'), | |
712 plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {}; | |
713 | |
714 // first generate and display notification text | |
715 $.each(alarms, function() { txt.push(this.title); }); | |
716 | |
717 rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000); | |
718 | |
719 // Internet Explorer does not support wav files, | |
720 // support in other browsers depends on enabled plugins, | |
721 // so we use wav as a fallback | |
722 src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav'; | |
723 | |
724 // HTML5 | |
725 try { | |
726 elem = $('<audio>').attr('src', src); | |
727 elem.get(0).play(); | |
728 } | |
729 // old method | |
730 catch (e) { | |
731 elem = $('<embed id="libcalsound" src="' + src + '" hidden=true autostart=true loop=false />'); | |
732 elem.appendTo($('body')); | |
733 window.setTimeout("$('#libcalsound').remove()", 10000); | |
734 } | |
735 }; | |
736 | |
737 /** | |
738 * Show a drop-down menu with a selection of snooze times | |
739 */ | |
740 this.snooze_dropdown = function(link, event) | |
741 { | |
742 if (!this.snooze_popup) { | |
743 this.snooze_popup = $('#alarm-snooze-dropdown'); | |
744 // create popup if not found | |
745 if (!this.snooze_popup.length) { | |
746 this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body); | |
747 this.snooze_popup.html(rcmail.env.snooze_select) | |
748 } | |
749 $('#alarm-snooze-dropdown a').click(function(e){ | |
750 var time = String(this.href).replace(/.+#/, ''); | |
751 me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time, e); | |
752 return false; | |
753 }); | |
754 } | |
755 | |
756 // hide visible popup | |
757 if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) { | |
758 rcmail.command('menu-close', 'alarm-snooze-dropdown', link.get(0), event); | |
759 this.dismiss_link = null; | |
760 } | |
761 else { // open popup below the clicked link | |
762 rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event); | |
763 this.snooze_popup.data('id', link.data('id')); | |
764 this.dismiss_link = link; | |
765 } | |
766 }; | |
767 | |
768 /** | |
769 * Dismiss or snooze alarms for the given event | |
770 */ | |
771 this.dismiss_alarm = function(id, snooze, event) | |
772 { | |
773 rcmail.command('menu-close', 'alarm-snooze-dropdown', null, event); | |
774 rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } }); | |
775 | |
776 // remove dismissed alarm from list | |
777 if (this.dismiss_link) { | |
778 this.dismiss_link.closest('div.alarm-item').hide(); | |
779 var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; }); | |
780 if (new_ids.length) | |
781 this.alarm_ids = new_ids; | |
782 else | |
783 this.alarm_dialog.dialog('close'); | |
784 } | |
785 | |
786 this.dismiss_link = null; | |
787 }; | |
788 | |
789 | |
790 /***** Recurrence form handling *****/ | |
791 | |
792 /** | |
793 * Install event handlers on recurrence form elements | |
794 */ | |
795 this.init_recurrence_edit = function(prefix) | |
796 { | |
797 // toggle recurrence frequency forms | |
798 $('#edit-recurrence-frequency').change(function(e){ | |
799 var freq = $(this).val().toLowerCase(); | |
800 $('.recurrence-form').hide(); | |
801 if (freq) { | |
802 $('#recurrence-form-'+freq).show(); | |
803 if (freq != 'rdate') | |
804 $('#recurrence-form-until').show(); | |
805 } | |
806 }); | |
807 $('#recurrence-form-rdate input.button.add').click(function(e){ | |
808 var dt, dv = $('#edit-recurrence-rdate-input').val(); | |
809 if (dv && (dt = me.parse_datetime('12:00', dv))) { | |
810 me.add_rdate(dt); | |
811 me.sort_rdates(); | |
812 $('#edit-recurrence-rdate-input').val('') | |
813 } | |
814 else { | |
815 $('#edit-recurrence-rdate-input').select(); | |
816 } | |
817 }); | |
818 $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){ | |
819 $(this).closest('li').remove(); | |
820 return false; | |
821 }); | |
822 | |
823 $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); | |
824 $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); }); | |
825 $('#edit-recurrence-rdate-input').datepicker(datepicker_settings); | |
826 }; | |
827 | |
828 /** | |
829 * Set recurrence form according to the given event/task record | |
830 */ | |
831 this.set_recurrence_edit = function(rec) | |
832 { | |
833 var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(), | |
834 interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1), | |
835 rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1), | |
836 rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : ''); | |
837 $('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false); | |
838 $('#edit-recurrence-rdates').html(''); | |
839 | |
840 var weekdays = ['SU','MO','TU','WE','TH','FR','SA'], | |
841 rrepeat_id = '#edit-recurrence-repeat-forever'; | |
842 if (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count'; | |
843 else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until'; | |
844 $(rrepeat_id).prop('checked', true); | |
845 | |
846 if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') { | |
847 var wdays = rec.recurrence.BYDAY.split(','); | |
848 $('input.edit-recurrence-weekly-byday').val(wdays); | |
849 } | |
850 if (rec.recurrence && rec.recurrence.BYMONTHDAY) { | |
851 $('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(',')); | |
852 $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']); | |
853 } | |
854 if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) { | |
855 var byday, section = rec.recurrence.FREQ.toLowerCase(); | |
856 if ((byday = String(rec.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) { | |
857 $('#edit-recurrence-'+section+'-prefix').val(byday[1]); | |
858 $('#edit-recurrence-'+section+'-byday').val(byday[2]); | |
859 } | |
860 $('input.edit-recurrence-'+section+'-mode').val(['BYDAY']); | |
861 } | |
862 else if (rec.start) { | |
863 $('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]); | |
864 } | |
865 if (rec.recurrence && rec.recurrence.BYMONTH) { | |
866 $('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(',')); | |
867 } | |
868 else if (rec.start) { | |
869 $('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]); | |
870 } | |
871 if (rec.recurrence && rec.recurrence.RDATE) { | |
872 $.each(rec.recurrence.RDATE, function(i,rdate){ | |
873 me.add_rdate(me.parseISO8601(rdate)); | |
874 }); | |
875 } | |
876 }; | |
877 | |
878 /** | |
879 * Gather recurrence settings from form | |
880 */ | |
881 this.serialize_recurrence = function(timestr) | |
882 { | |
883 var recurrence = '', | |
884 freq = $('#edit-recurrence-frequency').val(); | |
885 | |
886 if (freq != '') { | |
887 recurrence = { | |
888 FREQ: freq, | |
889 INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val() | |
890 }; | |
891 | |
892 var until = $('input.edit-recurrence-until:checked').val(); | |
893 if (until == 'count') | |
894 recurrence.COUNT = $('#edit-recurrence-repeat-times').val(); | |
895 else if (until == 'until') | |
896 recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val())); | |
897 | |
898 if (freq == 'WEEKLY') { | |
899 var byday = []; | |
900 $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); }); | |
901 if (byday.length) | |
902 recurrence.BYDAY = byday.join(','); | |
903 } | |
904 else if (freq == 'MONTHLY') { | |
905 var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = []; | |
906 if (mode == 'BYMONTHDAY') { | |
907 $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); }); | |
908 if (bymonday.length) | |
909 recurrence.BYMONTHDAY = bymonday.join(','); | |
910 } | |
911 else | |
912 recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val(); | |
913 } | |
914 else if (freq == 'YEARLY') { | |
915 var byday, bymonth = []; | |
916 $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); }); | |
917 if (bymonth.length) | |
918 recurrence.BYMONTH = bymonth.join(','); | |
919 if ((byday = $('#edit-recurrence-yearly-byday').val())) | |
920 recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday; | |
921 } | |
922 else if (freq == 'RDATE') { | |
923 recurrence = { RDATE:[] }; | |
924 // take selected but not yet added date into account | |
925 if ($('#edit-recurrence-rdate-input').val() != '') { | |
926 $('#recurrence-form-rdate input.button.add').click(); | |
927 } | |
928 $('#edit-recurrence-rdates li').each(function(i, li){ | |
929 recurrence.RDATE.push($(li).attr('data-value')); | |
930 }); | |
931 } | |
932 } | |
933 | |
934 return recurrence; | |
935 }; | |
936 | |
937 // add the given date to the RDATE list | |
938 this.add_rdate = function(date) | |
939 { | |
940 var li = $('<li>') | |
941 .attr('data-value', this.date2ISO8601(date)) | |
942 .html('<span>' + Q(this.format_datetime(date, 1)) + '</span>') | |
943 .appendTo('#edit-recurrence-rdates'); | |
944 | |
945 $('<a>').attr('href', '#del') | |
946 .addClass('iconbutton delete') | |
947 .html(rcmail.get_label('delete', 'libcalendaring')) | |
948 .attr('title', rcmail.get_label('delete', 'libcalendaring')) | |
949 .appendTo(li); | |
950 }; | |
951 | |
952 // re-sort the list items by their 'data-value' attribute | |
953 this.sort_rdates = function() | |
954 { | |
955 var mylist = $('#edit-recurrence-rdates'), | |
956 listitems = mylist.children('li').get(); | |
957 listitems.sort(function(a, b) { | |
958 var compA = $(a).attr('data-value'); | |
959 var compB = $(b).attr('data-value'); | |
960 return (compA < compB) ? -1 : (compA > compB) ? 1 : 0; | |
961 }) | |
962 $.each(listitems, function(idx, item) { mylist.append(item); }); | |
963 }; | |
964 | |
965 | |
966 /***** Attendee form handling *****/ | |
967 | |
968 // expand the given contact group into individual event/task attendees | |
969 this.expand_attendee_group = function(e, add, remove) | |
970 { | |
971 var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'), | |
972 role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected'); | |
973 | |
974 this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove } | |
975 | |
976 // copy group role from the according form element | |
977 if (role_select.length) { | |
978 this.group2expand[id].data.role = role_select.val(); | |
979 } | |
980 | |
981 // register callback handler | |
982 if (!this._expand_attendee_listener) { | |
983 this._expand_attendee_listener = this.expand_attendee_callback; | |
984 rcmail.addEventListener('plugin.expand_attendee_callback', function(result) { | |
985 me._expand_attendee_listener(result); | |
986 }); | |
987 } | |
988 | |
989 rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading')); | |
990 }; | |
991 | |
992 // callback from server to expand an attendee group | |
993 this.expand_attendee_callback = function(result) | |
994 { | |
995 var attendee, id = result.id, | |
996 data = this.group2expand[id], | |
997 row = $(data.link).closest('tr'); | |
998 | |
999 // replace group entry with all members returned by the server | |
1000 if (data && data.adder && result.members && result.members.length) { | |
1001 for (var i=0; i < result.members.length; i++) { | |
1002 attendee = result.members[i]; | |
1003 attendee.role = data.data.role; | |
1004 attendee.cutype = 'INDIVIDUAL'; | |
1005 attendee.status = 'NEEDS-ACTION'; | |
1006 data.adder(attendee, null, row); | |
1007 } | |
1008 | |
1009 if (data.remover) { | |
1010 data.remover(data.link, id) | |
1011 } | |
1012 else { | |
1013 row.remove(); | |
1014 } | |
1015 | |
1016 delete this.group2expand[id]; | |
1017 } | |
1018 else { | |
1019 rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error'); | |
1020 } | |
1021 }; | |
1022 | |
1023 | |
1024 // Render message reference links to the given container | |
1025 this.render_message_links = function(links, container, edit, plugin) | |
1026 { | |
1027 var ul = $('<ul>').addClass('attachmentslist'); | |
1028 | |
1029 $.each(links, function(i, link) { | |
1030 if (!link.mailurl) | |
1031 return true; // continue | |
1032 | |
1033 var li = $('<li>').addClass('link') | |
1034 .addClass('message eml') | |
1035 .append($('<a>') | |
1036 .attr('href', link.mailurl) | |
1037 .addClass('messagelink') | |
1038 .text(link.subject || link.uri) | |
1039 ) | |
1040 .appendTo(ul); | |
1041 | |
1042 // add icon to remove the link | |
1043 if (edit) { | |
1044 $('<a>') | |
1045 .attr('href', '#delete') | |
1046 .attr('title', rcmail.gettext('removelink', plugin)) | |
1047 .attr('data-uri', link.uri) | |
1048 .addClass('delete') | |
1049 .text(rcmail.gettext('delete')) | |
1050 .appendTo(li); | |
1051 } | |
1052 }); | |
1053 | |
1054 container.empty().append(ul); | |
1055 } | |
1056 | |
1057 // resize and reposition (center) the dialog window | |
1058 this.dialog_resize = function(id, height, width) | |
1059 { | |
1060 var win = $(window), w = win.width(), h = win.height(); | |
1061 | |
1062 $(id).dialog('option', { | |
1063 height: Math.min(h-20, height+130), | |
1064 width: Math.min(w-20, width+50) | |
1065 }); | |
1066 }; | |
1067 } | |
1068 | |
1069 ////// static methods | |
1070 | |
1071 // render HTML code for displaying an attendee record | |
1072 rcube_libcalendaring.attendee_html = function(data) | |
1073 { | |
1074 var name, tooltip = '', context = 'libcalendaring', | |
1075 dispname = data.name || data.email, | |
1076 status = data.role == 'ORGANIZER' ? 'ORGANIZER' : data.status; | |
1077 | |
1078 if (status) | |
1079 status = status.toLowerCase(); | |
1080 | |
1081 if (data.email) { | |
1082 tooltip = data.email; | |
1083 name = $('<a>').attr({href: 'mailto:' + data.email, 'class': 'mailtolink', 'data-cutype': data.cutype}) | |
1084 | |
1085 if (status) | |
1086 tooltip += ' (' + rcmail.gettext('status' + status, context) + ')'; | |
1087 } | |
1088 else { | |
1089 name = $('<span>'); | |
1090 } | |
1091 | |
1092 if (data['delegated-to']) | |
1093 tooltip = rcmail.gettext('delegatedto', context) + ' ' + data['delegated-to']; | |
1094 else if (data['delegated-from']) | |
1095 tooltip = rcmail.gettext('delegatedfrom', context) + ' ' + data['delegated-from']; | |
1096 | |
1097 return $('<span>').append( | |
1098 $('<span>').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname)) | |
1099 ).html(); | |
1100 }; | |
1101 | |
1102 /** | |
1103 * | |
1104 */ | |
1105 rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id) | |
1106 { | |
1107 // ask user to delete the declined event from the local calendar (#1670) | |
1108 var del = false; | |
1109 if (rcmail.env.rsvp_saved && status == 'declined') { | |
1110 del = confirm(rcmail.gettext('itip.declinedeleteconfirm')); | |
1111 } | |
1112 | |
1113 // open dialog for iTip delegation | |
1114 if (status == 'delegated') { | |
1115 rcube_libcalendaring.itip_delegate_dialog(function(data) { | |
1116 rcmail.http_post(task + '/itip-delegate', { | |
1117 _uid: rcmail.env.uid, | |
1118 _mbox: rcmail.env.mailbox, | |
1119 _part: mime_id, | |
1120 _to: data.to, | |
1121 _rsvp: data.rsvp ? 1 : 0, | |
1122 _comment: data.comment, | |
1123 _folder: data.target | |
1124 }, rcmail.set_busy(true, 'itip.savingdata')); | |
1125 }, $('#rsvp-'+dom_id+' .folder-select')); | |
1126 return false; | |
1127 } | |
1128 | |
1129 var noreply = 0, comment = ''; | |
1130 if (dom_id) { | |
1131 noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0; | |
1132 if (!noreply) | |
1133 comment = $('#reply-comment-'+dom_id).val(); | |
1134 } | |
1135 | |
1136 rcmail.http_post(task + '/mailimportitip', { | |
1137 _uid: rcmail.env.uid, | |
1138 _mbox: rcmail.env.mailbox, | |
1139 _part: mime_id, | |
1140 _folder: $('#itip-saveto').val(), | |
1141 _status: status, | |
1142 _del: del?1:0, | |
1143 _noreply: noreply, | |
1144 _comment: comment | |
1145 }, rcmail.set_busy(true, 'itip.savingdata')); | |
1146 | |
1147 return false; | |
1148 }; | |
1149 | |
1150 /** | |
1151 * Helper function to render the iTip delegation dialog | |
1152 * and trigger a callback function when submitted. | |
1153 */ | |
1154 rcube_libcalendaring.itip_delegate_dialog = function(callback, selector) | |
1155 { | |
1156 // show dialog for entering the delegatee address and comment | |
1157 var html = '<form class="itip-dialog-form" action="javascript:void()">' + | |
1158 '<div class="form-section">' + | |
1159 '<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' + | |
1160 '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' + | |
1161 '</div>' + | |
1162 '<div class="form-section">' + | |
1163 '<label for="itip-delegate-rsvp">' + | |
1164 '<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' + | |
1165 rcmail.gettext('itip.delegatersvpme') + | |
1166 '</label>' + | |
1167 '</div>' + | |
1168 '<div class="form-section">' + | |
1169 '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' + | |
1170 rcmail.gettext('itip.itipcomment') + '"></textarea>' + | |
1171 '</div>' + | |
1172 '<div class="form-section">' + | |
1173 (selector && selector.length ? selector.html() : '') + | |
1174 '</div>' + | |
1175 '</form>'; | |
1176 | |
1177 var dialog, buttons = []; | |
1178 buttons.push({ | |
1179 text: rcmail.gettext('itipdelegated', 'itip'), | |
1180 click: function() { | |
1181 var doc = window.parent.document, | |
1182 delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, ''); | |
1183 | |
1184 if (delegatee != '' && rcube_check_email(delegatee, true)) { | |
1185 callback({ | |
1186 to: delegatee, | |
1187 rsvp: $('#itip-delegate-rsvp', doc).prop('checked'), | |
1188 comment: $('#itip-delegate-comment', doc).val(), | |
1189 target: $('#itip-saveto', doc).val() | |
1190 }); | |
1191 | |
1192 setTimeout(function() { dialog.dialog("close"); }, 500); | |
1193 } | |
1194 else { | |
1195 alert(rcmail.gettext('itip.delegateinvalidaddress')); | |
1196 $('#itip-delegate-to', doc).focus(); | |
1197 } | |
1198 } | |
1199 }); | |
1200 | |
1201 buttons.push({ | |
1202 text: rcmail.gettext('cancel', 'itip'), | |
1203 click: function() { | |
1204 dialog.dialog('close'); | |
1205 } | |
1206 }); | |
1207 | |
1208 dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, { | |
1209 width: 460, | |
1210 open: function(event, ui) { | |
1211 $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction'); | |
1212 $(this).find('#itip-saveto').val(''); | |
1213 | |
1214 // initialize autocompletion | |
1215 var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail; | |
1216 if (rcmail.env.autocomplete_threads > 0) { | |
1217 ac_props = { | |
1218 threads: rcmail.env.autocomplete_threads, | |
1219 sources: rcmail.env.autocomplete_sources | |
1220 }; | |
1221 } | |
1222 rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props); | |
1223 rcm.env.recipients_delimiter = ''; | |
1224 }, | |
1225 close: function(event, ui) { | |
1226 rcm = rcmail.is_framed() ? parent.rcmail : rcmail; | |
1227 rcm.ksearch_blur(); | |
1228 $(this).remove(); | |
1229 } | |
1230 }); | |
1231 | |
1232 return dialog; | |
1233 }; | |
1234 | |
1235 /** | |
1236 * Show a menu for selecting the RSVP reply mode | |
1237 */ | |
1238 rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback) | |
1239 { | |
1240 var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode'); | |
1241 | |
1242 $.each(['all','current'/*,'future'*/], function(i, mode) { | |
1243 $('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>') | |
1244 .addClass('ui-menu-item') | |
1245 .attr('rel', mode) | |
1246 .appendTo(mnu); | |
1247 }); | |
1248 | |
1249 var action = btn.attr('rel'); | |
1250 | |
1251 // open the mennu | |
1252 mnu.menu({ | |
1253 select: function(event, ui) { | |
1254 callback(action, ui.item.attr('rel')); | |
1255 } | |
1256 }) | |
1257 .appendTo(document.body) | |
1258 .position({ my: 'left top', at: 'left bottom+2', of: btn }) | |
1259 .data('action', action); | |
1260 | |
1261 setTimeout(function() { | |
1262 $(document).one('click', function() { | |
1263 mnu.menu('destroy'); | |
1264 mnu.remove(); | |
1265 }); | |
1266 }, 100); | |
1267 }; | |
1268 | |
1269 /** | |
1270 * | |
1271 */ | |
1272 rcube_libcalendaring.remove_from_itip = function(event, task, title) | |
1273 { | |
1274 if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) { | |
1275 rcmail.http_post(task + '/itip-remove', | |
1276 event, | |
1277 rcmail.set_busy(true, 'itip.savingdata') | |
1278 ); | |
1279 } | |
1280 }; | |
1281 | |
1282 /** | |
1283 * | |
1284 */ | |
1285 rcube_libcalendaring.decline_attendee_reply = function(mime_id, task) | |
1286 { | |
1287 // show dialog for entering a comment and send to server | |
1288 var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' + | |
1289 '<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>'; | |
1290 | |
1291 var dialog, buttons = []; | |
1292 buttons.push({ | |
1293 text: rcmail.gettext('declineattendee', 'itip'), | |
1294 click: function() { | |
1295 rcmail.http_post(task + '/itip-decline-reply', { | |
1296 _uid: rcmail.env.uid, | |
1297 _mbox: rcmail.env.mailbox, | |
1298 _part: mime_id, | |
1299 _comment: $('#itip-decline-comment', window.parent.document).val() | |
1300 }, rcmail.set_busy(true, 'itip.savingdata')); | |
1301 dialog.dialog("close"); | |
1302 } | |
1303 }); | |
1304 | |
1305 buttons.push({ | |
1306 text: rcmail.gettext('cancel', 'itip'), | |
1307 click: function() { | |
1308 dialog.dialog('close'); | |
1309 } | |
1310 }); | |
1311 | |
1312 dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, { | |
1313 width: 460, | |
1314 open: function() { | |
1315 $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction'); | |
1316 $('#itip-decline-comment').focus(); | |
1317 } | |
1318 }); | |
1319 | |
1320 return false; | |
1321 }; | |
1322 | |
1323 /** | |
1324 * | |
1325 */ | |
1326 rcube_libcalendaring.fetch_itip_object_status = function(p) | |
1327 { | |
1328 rcmail.http_post(p.task + '/itip-status', { data: p }); | |
1329 }; | |
1330 | |
1331 /** | |
1332 * | |
1333 */ | |
1334 rcube_libcalendaring.update_itip_object_status = function(p) | |
1335 { | |
1336 rcmail.env.rsvp_saved = p.saved; | |
1337 rcmail.env.itip_existing = p.existing; | |
1338 | |
1339 // hide all elements first | |
1340 $('#itip-buttons-'+p.id+' > div').hide(); | |
1341 $('#rsvp-'+p.id+' .folder-select').remove(); | |
1342 | |
1343 if (p.html) { | |
1344 // append/replace rsvp status display | |
1345 $('#loading-'+p.id).next('.rsvp-status').remove(); | |
1346 $('#loading-'+p.id).hide().after(p.html); | |
1347 } | |
1348 | |
1349 // enable/disable rsvp buttons | |
1350 if (p.action == 'rsvp') { | |
1351 $('#rsvp-'+p.id+' input.button').prop('disabled', false) | |
1352 .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest); | |
1353 } | |
1354 | |
1355 // show rsvp/import buttons (with calendar selector) | |
1356 $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select); | |
1357 | |
1358 // highlight date if date change detected | |
1359 if (p.resheduled) | |
1360 $('.calendar-eventdetails td.date').addClass('modified'); | |
1361 | |
1362 // show itip box appendix after replacing the given placeholders | |
1363 if (p.append && p.append.selector) { | |
1364 var elem = $(p.append.selector); | |
1365 if (p.append.replacements) { | |
1366 $.each(p.append.replacements, function(k, html) { | |
1367 elem.html(elem.html().replace(k, html)); | |
1368 }); | |
1369 } | |
1370 else if (p.append.html) { | |
1371 elem.html(p.append.html) | |
1372 } | |
1373 elem.show(); | |
1374 } | |
1375 }; | |
1376 | |
1377 /** | |
1378 * Callback from server after an iTip message has been processed | |
1379 */ | |
1380 rcube_libcalendaring.itip_message_processed = function(metadata) | |
1381 { | |
1382 if (metadata.after_action) { | |
1383 setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200); | |
1384 } | |
1385 else { | |
1386 rcube_libcalendaring.fetch_itip_object_status(metadata); | |
1387 } | |
1388 }; | |
1389 | |
1390 /** | |
1391 * After-action on iTip request message. Action types: | |
1392 * 0 - no action | |
1393 * 1 - move to Trash | |
1394 * 2 - delete the message | |
1395 * 3 - flag as deleted | |
1396 * folder_name - move the message to the specified folder | |
1397 */ | |
1398 rcube_libcalendaring.itip_after_action = function(action) | |
1399 { | |
1400 if (!action) { | |
1401 return; | |
1402 } | |
1403 | |
1404 var rc = rcmail.is_framed() ? parent.rcmail : rcmail; | |
1405 | |
1406 if (action === 2) { | |
1407 rc.permanently_remove_messages(); | |
1408 } | |
1409 else if (action === 3) { | |
1410 rc.mark_message('delete'); | |
1411 } | |
1412 else { | |
1413 rc.move_messages(action === 1 ? rc.env.trash_mailbox : action); | |
1414 } | |
1415 }; | |
1416 | |
1417 /** | |
1418 * Open the calendar preview for the current iTip event | |
1419 */ | |
1420 rcube_libcalendaring.open_itip_preview = function(url, msgref) | |
1421 { | |
1422 if (!rcmail.env.itip_existing) | |
1423 url += '&itip=' + escape(msgref); | |
1424 | |
1425 var win = rcmail.open_window(url); | |
1426 }; | |
1427 | |
1428 | |
1429 // extend jQuery | |
1430 (function($){ | |
1431 $.fn.serializeJSON = function(){ | |
1432 var json = {}; | |
1433 jQuery.map($(this).serializeArray(), function(n, i) { | |
1434 json[n['name']] = n['value']; | |
1435 }); | |
1436 return json; | |
1437 }; | |
1438 })(jQuery); | |
1439 | |
1440 | |
1441 /* libcalendaring plugin initialization */ | |
1442 window.rcmail && rcmail.addEventListener('init', function(evt) { | |
1443 if (rcmail.env.libcal_settings) { | |
1444 var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings); | |
1445 rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); }); | |
1446 } | |
1447 | |
1448 rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status) | |
1449 .addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status) | |
1450 .addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed); | |
1451 | |
1452 if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects['attachmentframe']) { | |
1453 rcmail.register_command('print-attachment', function() { | |
1454 var frame = rcmail.get_frame_window(rcmail.gui_objects['attachmentframe'].id); | |
1455 if (frame) frame.print(); | |
1456 }, true); | |
1457 } | |
1458 | |
1459 if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) { | |
1460 rcmail.register_command('download-attachment', function() { | |
1461 rcmail.location_href(rcmail.env.attachment_download_url, window); | |
1462 }, true); | |
1463 } | |
1464 }); |