1. 1 : /**
  2. 2 : * Kolab groupware utilities
  3. 3 : *
  4. 4 : * @author Thomas Bruederli <bruederli@kolabsys.com>
  5. 5 : *
  6. 6 : * @licstart The following is the entire license notice for the
  7. 7 : * JavaScript code in this file.
  8. 8 : *
  9. 9 : * Copyright (C) 2015-2018, Kolab Systems AG <contact@kolabsys.com>
  10. 10 : *
  11. 11 : * This program is free software: you can redistribute it and/or modify
  12. 12 : * it under the terms of the GNU Affero General Public License as
  13. 13 : * published by the Free Software Foundation, either version 3 of the
  14. 14 : * License, or (at your option) any later version.
  15. 15 : *
  16. 16 : * This program is distributed in the hope that it will be useful,
  17. 17 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. 18 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. 19 : * GNU Affero General Public License for more details.
  20. 20 : *
  21. 21 : * You should have received a copy of the GNU Affero General Public License
  22. 22 : * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. 23 : *
  24. 24 : * @licend The above is the entire license notice
  25. 25 : * for the JavaScript code in this file.
  26. 26 : */
  27. 27 :
  28. 28 : var libkolab_audittrail = {}, libkolab = {};
  29. 29 :
  30. 30 : libkolab_audittrail.quote_html = function(str)
  31. 31 : {
  32. 32 : return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  33. 33 : };
  34. 34 :
  35. 35 : // show object changelog in a dialog
  36. 36 : libkolab_audittrail.object_history_dialog = function(p)
  37. 37 : {
  38. 38 : // render dialog
  39. 39 : var $dialog = $(p.container);
  40. 40 :
  41. 41 : // close show dialog first
  42. 42 : if ($dialog.is(':ui-dialog'))
  43. 43 : $dialog.dialog('close');
  44. 44 :
  45. 45 : // hide and reset changelog table
  46. 46 : $dialog.find('div.notfound-message').remove();
  47. 47 : $dialog.find('.changelog-table').show().children('tbody')
  48. 48 : .html('<tr><td colspan="4"><span class="loading">' + rcmail.gettext('loading') + '</span></td></tr>');
  49. 49 :
  50. 50 : // open jquery UI dialog
  51. 51 : $dialog.dialog({
  52. 52 : modal: true,
  53. 53 : resizable: true,
  54. 54 : closeOnEscape: true,
  55. 55 : title: p.title,
  56. 56 : open: function() {
  57. 57 : $dialog.attr('aria-hidden', 'false');
  58. 58 : },
  59. 59 : close: function() {
  60. 60 : $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
  61. 61 : },
  62. 62 : buttons: [
  63. 63 : {
  64. 64 : text: rcmail.gettext('close'),
  65. 65 : click: function() { $dialog.dialog('close'); },
  66. 66 : 'class': 'cancel',
  67. 67 : autofocus: true
  68. 68 : }
  69. 69 : ],
  70. 70 : minWidth: 450,
  71. 71 : width: 650,
  72. 72 : height: 350,
  73. 73 : minHeight: 200
  74. 74 : })
  75. 75 : .show().children('.compare-button').hide();
  76. 76 :
  77. 77 : // initialize event handlers for history dialog UI elements
  78. 78 : if (!$dialog.data('initialized')) {
  79. 79 : // compare button
  80. 80 : $dialog.find('.compare-button input').click(function(e) {
  81. 81 : var rev1 = $dialog.find('.changelog-table input.diff-rev1:checked').val(),
  82. 82 : rev2 = $dialog.find('.changelog-table input.diff-rev2:checked').val();
  83. 83 :
  84. 84 : if (rev1 && rev2 && rev1 != rev2) {
  85. 85 : // swap revisions if the user got it wrong
  86. 86 : if (rev1 > rev2) {
  87. 87 : var tmp = rev2;
  88. 88 : rev2 = rev1;
  89. 89 : rev1 = tmp;
  90. 90 : }
  91. 91 :
  92. 92 : if (p.comparefunc) {
  93. 93 : p.comparefunc(rev1, rev2);
  94. 94 : }
  95. 95 : }
  96. 96 : else {
  97. 97 : alert('Invalid selection!')
  98. 98 : }
  99. 99 :
  100. 100 : if (!rcube_event.is_keyboard(e) && this.blur) {
  101. 101 : this.blur();
  102. 102 : }
  103. 103 : return false;
  104. 104 : });
  105. 105 :
  106. 106 : // delegate handlers for list actions
  107. 107 : $dialog.find('.changelog-table tbody').on('click', 'td.actions a', function(e) {
  108. 108 : var link = $(this),
  109. 109 : action = link.hasClass('restore') ? 'restore' : 'show',
  110. 110 : event = $('#eventhistory').data('event'),
  111. 111 : rev = link.attr('data-rev');
  112. 112 :
  113. 113 : // ignore clicks on first row (current revision)
  114. 114 : if (link.closest('tr').hasClass('first')) {
  115. 115 : return false;
  116. 116 : }
  117. 117 :
  118. 118 : // let the user confirm the restore action
  119. 119 : if (action == 'restore' && !confirm(rcmail.gettext('revisionrestoreconfirm', p.module).replace('$rev', rev))) {
  120. 120 : return false;
  121. 121 : }
  122. 122 :
  123. 123 : if (p.listfunc) {
  124. 124 : p.listfunc(action, rev);
  125. 125 : }
  126. 126 :
  127. 127 : if (!rcube_event.is_keyboard(e) && this.blur) {
  128. 128 : this.blur();
  129. 129 : }
  130. 130 : return false;
  131. 131 : })
  132. 132 : .on('click', 'input.diff-rev1', function(e) {
  133. 133 : if (!this.checked) return true;
  134. 134 :
  135. 135 : var rev1 = this.value, selection_valid = false;
  136. 136 : $dialog.find('.changelog-table input.diff-rev2').each(function(i, elem) {
  137. 137 : $(elem).prop('disabled', elem.value <= rev1);
  138. 138 : if (elem.checked && elem.value > rev1) {
  139. 139 : selection_valid = true;
  140. 140 : }
  141. 141 : });
  142. 142 : if (!selection_valid) {
  143. 143 : $dialog.find('.changelog-table input.diff-rev2:not([disabled])').last().prop('checked', true);
  144. 144 : }
  145. 145 : });
  146. 146 :
  147. 147 : $dialog.addClass('changelog-dialog').data('initialized', true);
  148. 148 : }
  149. 149 :
  150. 150 : return $dialog;
  151. 151 : };
  152. 152 :
  153. 153 : // callback from server with changelog data
  154. 154 : libkolab_audittrail.render_changelog = function(data, object, folder)
  155. 155 : {
  156. 156 : var Q = libkolab_audittrail.quote_html;
  157. 157 :
  158. 158 : var $dialog = $('.changelog-dialog')
  159. 159 : if (data === false || !data.length) {
  160. 160 : return false;
  161. 161 : }
  162. 162 :
  163. 163 : var i, change, accessible, op_append,
  164. 164 : first = data.length - 1, last = 0,
  165. 165 : is_writeable = !!folder.editable,
  166. 166 : op_labels = {
  167. 167 : RECEIVE: 'actionreceive',
  168. 168 : APPEND: 'actionappend',
  169. 169 : MOVE: 'actionmove',
  170. 170 : DELETE: 'actiondelete',
  171. 171 : READ: 'actionread',
  172. 172 : FLAGSET: 'actionflagset',
  173. 173 : FLAGCLEAR: 'actionflagclear'
  174. 174 : },
  175. 175 : actions = '<a href="#show" class="iconbutton preview" title="'+ rcmail.gettext('showrevision','libkolab') +'" data-rev="{rev}" /> ' +
  176. 176 : (is_writeable ? '<a href="#restore" class="iconbutton restore" title="'+ rcmail.gettext('restore','libkolab') + '" data-rev="{rev}" />' : ''),
  177. 177 : tbody = $dialog.find('.changelog-table tbody').html('');
  178. 178 :
  179. 179 : for (i=first; i >= 0; i--) {
  180. 180 : change = data[i];
  181. 181 : accessible = change.date && change.user;
  182. 182 :
  183. 183 : if (change.op == 'MOVE' && change.mailbox) {
  184. 184 : op_append = ' ⇢ ' + change.mailbox;
  185. 185 : }
  186. 186 : else if ((change.op == 'FLAGSET' || change.op == 'FLAGCLEAR') && change.flags) {
  187. 187 : op_append = ': ' + change.flags;
  188. 188 : }
  189. 189 : else {
  190. 190 : op_append = '';
  191. 191 : }
  192. 192 :
  193. 193 : $('<tr class="' + (i == first ? 'first' : (i == last ? 'last' : '')) + (accessible ? '' : 'undisclosed') + '">')
  194. 194 : .append('<td class="diff">' + (accessible && change.op != 'DELETE' ?
  195. 195 : '<input type="radio" name="rev1" class="diff-rev1" value="' + change.rev + '" title="" '+ (i == last ? 'checked="checked"' : '') +' /> '+
  196. 196 : '<input type="radio" name="rev2" class="diff-rev2" value="' + change.rev + '" title="" '+ (i == first ? 'checked="checked"' : '') +' /></td>'
  197. 197 : : ''))
  198. 198 : .append('<td class="revision">' + Q(i+1) + '</td>')
  199. 199 : .append('<td class="date">' + Q(change.date || '') + '</td>')
  200. 200 : .append('<td class="user">' + Q(change.user || 'undisclosed') + '</td>')
  201. 201 : .append('<td class="operation" title="' + op_append + '">' + Q(rcmail.gettext(op_labels[change.op] || '', 'libkolab') + op_append) + '</td>')
  202. 202 : .append('<td class="actions">' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + '</td>')
  203. 203 : .appendTo(tbody);
  204. 204 : }
  205. 205 :
  206. 206 : if (first > 0) {
  207. 207 : $dialog.find('.compare-button').fadeIn(200);
  208. 208 : $dialog.find('.changelog-table tr.last input.diff-rev1').click();
  209. 209 : }
  210. 210 :
  211. 211 : // set dialog size according to content
  212. 212 : libkolab_audittrail.dialog_resize($dialog.get(0), $dialog.height() + 15, 600);
  213. 213 :
  214. 214 : return $dialog;
  215. 215 : };
  216. 216 :
  217. 217 : // resize and reposition (center) the dialog window
  218. 218 : libkolab_audittrail.dialog_resize = function(id, height, width)
  219. 219 : {
  220. 220 : var win = $(window), w = win.width(), h = win.height();
  221. 221 : $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) });
  222. 222 : };
  223. 223 :
  224. 224 : /**
  225. 225 : * Open an attachment either in a browser window for inline view or download it
  226. 226 : */
  227. 227 : libkolab.load_attachment = function(query, attachment)
  228. 228 : {
  229. 229 : query._frame = 1;
  230. 230 :
  231. 231 : // open attachment in frame if it's of a supported mimetype similar as in app.js
  232. 232 : if (attachment.id && attachment.mimetype && $.inArray(attachment.mimetype, rcmail.env.mimetypes) >= 0) {
  233. 233 : if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
  234. 234 : return;
  235. 235 : }
  236. 236 : }
  237. 237 :
  238. 238 : query._frame = null;
  239. 239 : query._download = 1;
  240. 240 : rcmail.goto_url('get-attachment', query, false);
  241. 241 : };
  242. 242 :
  243. 243 : /**
  244. 244 : * Build attachments list element
  245. 245 : */
  246. 246 : libkolab.list_attachments = function(list, container, edit, data, ondelete, onload)
  247. 247 : {
  248. 248 : var ul = $('<ul>').addClass('attachmentslist');
  249. 249 :
  250. 250 : $.each(list || [], function(i, elem) {
  251. 251 : var li = $('<li>').addClass(elem.classname);
  252. 252 :
  253. 253 : // name/link
  254. 254 : $('<a>').attr({href: '#load', 'class': 'filename'})
  255. 255 : .append($('<span class="attachment-name">').text(elem.name))
  256. 256 : .click({record: data, attachment: elem}, function(e) {
  257. 257 : if (onload) {
  258. 258 : onload(e.data);
  259. 259 : }
  260. 260 : return false;
  261. 261 : })
  262. 262 : .appendTo(li);
  263. 263 :
  264. 264 : if (edit) {
  265. 265 : rcmail.env.attachments[elem.id] = elem;
  266. 266 : // delete link
  267. 267 : $('<a>').attr({href: '#delete', title: rcmail.gettext('delete'), 'class': 'delete'})
  268. 268 : .click({id: elem.id}, function(e) {
  269. 269 : $(this.parentNode).hide();
  270. 270 : delete rcmail.env.attachments[e.data.id];
  271. 271 : if (ondelete) {
  272. 272 : ondelete(e.data.id);
  273. 273 : }
  274. 274 : return false;
  275. 275 : })
  276. 276 : .appendTo(li);
  277. 277 : }
  278. 278 :
  279. 279 : ul.append(li);
  280. 280 : });
  281. 281 :
  282. 282 : if (edit && rcmail.gui_objects.attachmentlist) {
  283. 283 : ul.id = rcmail.gui_objects.attachmentlist.id;
  284. 284 : rcmail.gui_objects.attachmentlist = ul.get(0);
  285. 285 : }
  286. 286 :
  287. 287 : container.empty().append(ul);
  288. 288 : };
  289. 289 :
  290. 290 :
  291. 291 : function kolab_folderlist(node, p)
  292. 292 : {
  293. 293 : // extends treelist.js
  294. 294 : rcube_treelist_widget.call(this, node, p);
  295. 295 :
  296. 296 : // private vars
  297. 297 : var me = this;
  298. 298 : var search_results;
  299. 299 : var search_results_widget;
  300. 300 : var search_results_container;
  301. 301 : var listsearch_request;
  302. 302 : var search_messagebox;
  303. 303 :
  304. 304 : var Q = rcmail.quote_html;
  305. 305 :
  306. 306 : // render the results for folderlist search
  307. 307 : function render_search_results(results)
  308. 308 : {
  309. 309 : if (results.length) {
  310. 310 : // create treelist widget to present the search results
  311. 311 : if (!search_results_widget) {
  312. 312 : var list_id = (me.container.attr('id') || p.id_prefix || '0');
  313. 313 :
  314. 314 : search_results_container = $('<div class="searchresults"></div>')
  315. 315 : .html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
  316. 316 : .insertAfter(me.container);
  317. 317 :
  318. 318 : search_results_widget = new rcube_treelist_widget('<ul>', {
  319. 319 : id_prefix: p.id_prefix,
  320. 320 : id_encode: p.id_encode,
  321. 321 : id_decode: p.id_decode,
  322. 322 : selectable: false
  323. 323 : });
  324. 324 :
  325. 325 : // copy classes from main list
  326. 326 : search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
  327. 327 :
  328. 328 : // register click handler on search result's checkboxes to select the given item for listing
  329. 329 : search_results_widget.container
  330. 330 : .appendTo(search_results_container)
  331. 331 : .on('click', 'input[type=checkbox], a.subscribed, span.subscribed', function(e) {
  332. 332 : var node, has_children, li = $(this).closest('li'),
  333. 333 : id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
  334. 334 :
  335. 335 : if (p.id_decode)
  336. 336 : id = p.id_decode(id);
  337. 337 : node = search_results_widget.get_node(id);
  338. 338 : has_children = node.children && node.children.length;
  339. 339 :
  340. 340 : e.stopPropagation();
  341. 341 : e.bubbles = false;
  342. 342 :
  343. 343 : // activate + subscribe
  344. 344 : if ($(e.target).hasClass('subscribed')) {
  345. 345 : search_results[id].subscribed = true;
  346. 346 : $(e.target).attr('aria-checked', 'true');
  347. 347 : li.children().first()
  348. 348 : .toggleClass('subscribed')
  349. 349 : .find('input[type=checkbox]').get(0).checked = true;
  350. 350 :
  351. 351 : if (has_children && search_results[id].group == 'other user') {
  352. 352 : li.find('ul li > div').addClass('subscribed')
  353. 353 : .find('a.subscribed').attr('aria-checked', 'true');;
  354. 354 : }
  355. 355 : }
  356. 356 : else if (!this.checked) {
  357. 357 : return;
  358. 358 : }
  359. 359 :
  360. 360 : // copy item to the main list
  361. 361 : add_result2list(id, li, true);
  362. 362 :
  363. 363 : if (has_children) {
  364. 364 : li.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
  365. 365 : li.find('a.subscribed, span.subscribed').first().hide();
  366. 366 : }
  367. 367 : else {
  368. 368 : li.remove();
  369. 369 : }
  370. 370 :
  371. 371 : // set partial subscription status
  372. 372 : if (search_results[id].subscribed && search_results[id].parent && search_results[id].group == 'other') {
  373. 373 : parent_subscription_status($(me.get_item(id, true)));
  374. 374 : }
  375. 375 :
  376. 376 : // set focus to cloned checkbox
  377. 377 : if (rcube_event.is_keyboard(e)) {
  378. 378 : $(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
  379. 379 : }
  380. 380 : })
  381. 381 : .on('click', function(e) {
  382. 382 : var prop, id = String($(e.target).closest('li').attr('id')).replace(new RegExp('^'+p.id_prefix), '');
  383. 383 : if (p.id_decode)
  384. 384 : id = p.id_decode(id);
  385. 385 :
  386. 386 : if (!rcube_event.is_keyboard(e) && e.target.blur)
  387. 387 : e.target.blur();
  388. 388 :
  389. 389 : // forward event
  390. 390 : if (prop = search_results[id]) {
  391. 391 : e.data = prop;
  392. 392 : if (me.triggerEvent('click-item', e) === false) {
  393. 393 : e.stopPropagation();
  394. 394 : return false;
  395. 395 : }
  396. 396 : }
  397. 397 : });
  398. 398 : }
  399. 399 :
  400. 400 : // add results to list
  401. 401 : for (var prop, item, i=0; i < results.length; i++) {
  402. 402 : prop = results[i];
  403. 403 : item = $(prop.html);
  404. 404 : search_results[prop.id] = prop;
  405. 405 : search_results_widget.insert({
  406. 406 : id: prop.id,
  407. 407 : classes: [ prop.group || '' ],
  408. 408 : html: item,
  409. 409 : collapsed: true,
  410. 410 : virtual: prop.virtual
  411. 411 : }, prop.parent);
  412. 412 :
  413. 413 : // disable checkbox if item already exists in main list
  414. 414 : if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
  415. 415 : item.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
  416. 416 : item.find('a.subscribed, span.subscribed').hide();
  417. 417 : }
  418. 418 :
  419. 419 : prop.li = item.parent().get(0);
  420. 420 : me.triggerEvent('add-item', prop);
  421. 421 : }
  422. 422 :
  423. 423 : search_results_container.show();
  424. 424 : }
  425. 425 : }
  426. 426 :
  427. 427 : // helper method to (recursively) add a search result item to the main list widget
  428. 428 : function add_result2list(id, li, active)
  429. 429 : {
  430. 430 : var node = search_results_widget.get_node(id),
  431. 431 : prop = search_results[id],
  432. 432 : parent_id = prop.parent || null,
  433. 433 : has_children = node.children && node.children.length,
  434. 434 : dom_node = has_children ? li.children().first().clone(true, true) : li.children().first(),
  435. 435 : childs = [];
  436. 436 :
  437. 437 : // find parent node and insert at the right place
  438. 438 : if (parent_id && me.get_node(parent_id)) {
  439. 439 : dom_node.children('span,a').first().html(Q(prop.editname || prop.listname));
  440. 440 : }
  441. 441 : else if (parent_id && search_results[parent_id]) {
  442. 442 : // copy parent tree from search results
  443. 443 : add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
  444. 444 : }
  445. 445 : else if (parent_id) {
  446. 446 : // use full name for list display
  447. 447 : dom_node.children('span,a').first().html(Q(prop.name));
  448. 448 : }
  449. 449 :
  450. 450 : // replace virtual node with a real one
  451. 451 : if (me.get_node(id)) {
  452. 452 : $(me.get_item(id, true)).children().first()
  453. 453 : .replaceWith(dom_node)
  454. 454 : .removeClass('virtual');
  455. 455 : }
  456. 456 : else {
  457. 457 : // copy childs, too
  458. 458 : if (has_children && prop.group == 'other user') {
  459. 459 : for (var cid, j=0; j < node.children.length; j++) {
  460. 460 : if ((cid = node.children[j].id) && search_results[cid]) {
  461. 461 : childs.push(search_results_widget.get_node(cid));
  462. 462 : }
  463. 463 : }
  464. 464 : }
  465. 465 :
  466. 466 : // move this result item to the main list widget
  467. 467 : me.insert({
  468. 468 : id: id,
  469. 469 : classes: [ prop.group || '' ],
  470. 470 : virtual: prop.virtual,
  471. 471 : html: dom_node,
  472. 472 : level: node.level,
  473. 473 : collapsed: true,
  474. 474 : children: childs
  475. 475 : }, parent_id, prop.group);
  476. 476 : }
  477. 477 :
  478. 478 : delete prop.html;
  479. 479 : prop.active = active;
  480. 480 : me.triggerEvent('insert-item', { id: id, data: prop, item: li });
  481. 481 :
  482. 482 : // register childs, too
  483. 483 : if (childs.length) {
  484. 484 : for (var cid, j=0; j < node.children.length; j++) {
  485. 485 : if ((cid = node.children[j].id) && search_results[cid]) {
  486. 486 : prop = search_results[cid];
  487. 487 : delete prop.html;
  488. 488 : prop.active = false;
  489. 489 : me.triggerEvent('insert-item', { id: cid, data: prop });
  490. 490 : }
  491. 491 : }
  492. 492 : }
  493. 493 : }
  494. 494 :
  495. 495 : // update the given item's parent's (partial) subscription state
  496. 496 : function parent_subscription_status(li)
  497. 497 : {
  498. 498 : var top_li = li.closest(me.container.children('li')),
  499. 499 : all_childs = $('li > div:not(.treetoggle)', top_li),
  500. 500 : subscribed = all_childs.filter('.subscribed').length;
  501. 501 :
  502. 502 : if (subscribed == 0) {
  503. 503 : top_li.children('div:first').removeClass('subscribed partial');
  504. 504 : }
  505. 505 : else {
  506. 506 : top_li.children('div:first')
  507. 507 : .addClass('subscribed')[subscribed < all_childs.length ? 'addClass' : 'removeClass']('partial');
  508. 508 : }
  509. 509 : }
  510. 510 :
  511. 511 : // do some magic when search is performed on the widget
  512. 512 : this.addEventListener('search', function(search) {
  513. 513 : // hide search results
  514. 514 : if (search_results_widget) {
  515. 515 : search_results_container.hide();
  516. 516 : search_results_widget.reset();
  517. 517 : }
  518. 518 : search_results = {};
  519. 519 :
  520. 520 : if (search_messagebox)
  521. 521 : rcmail.hide_message(search_messagebox);
  522. 522 :
  523. 523 : // send search request(s) to server
  524. 524 : if (search.query && search.execute) {
  525. 525 : // require a minimum length for the search string
  526. 526 : if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length && search.query != '*') {
  527. 527 : search_messagebox = rcmail.display_message(
  528. 528 : rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
  529. 529 : return;
  530. 530 : }
  531. 531 :
  532. 532 : if (listsearch_request) {
  533. 533 : // ignore, let the currently running request finish
  534. 534 : if (listsearch_request.query == search.query) {
  535. 535 : return;
  536. 536 : }
  537. 537 : else { // cancel previous search request
  538. 538 : rcmail.multi_thread_request_abort(listsearch_request.id);
  539. 539 : listsearch_request = null;
  540. 540 : }
  541. 541 : }
  542. 542 :
  543. 543 : var sources = p.search_sources || [ 'folders' ];
  544. 544 : var reqid = rcmail.multi_thread_http_request({
  545. 545 : items: sources,
  546. 546 : threads: rcmail.env.autocomplete_threads || 1,
  547. 547 : action: p.search_action || 'listsearch',
  548. 548 : postdata: { action:'search', q:search.query, source:'%s' },
  549. 549 : lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
  550. 550 : onresponse: render_search_results,
  551. 551 : whendone: function(data){
  552. 552 : listsearch_request = null;
  553. 553 : me.triggerEvent('search-complete', data);
  554. 554 : }
  555. 555 : });
  556. 556 :
  557. 557 : listsearch_request = { id:reqid, query:search.query };
  558. 558 : }
  559. 559 : else if (!search.query && listsearch_request) {
  560. 560 : rcmail.multi_thread_request_abort(listsearch_request.id);
  561. 561 : listsearch_request = null;
  562. 562 : }
  563. 563 : });
  564. 564 :
  565. 565 : this.container.on('click', 'a.subscribed, span.subscribed', function(e) {
  566. 566 : var li = $(this).closest('li'),
  567. 567 : id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
  568. 568 : div = li.children().first(),
  569. 569 : is_subscribed;
  570. 570 :
  571. 571 : if (me.is_search()) {
  572. 572 : id = id.replace(/--xsR$/, '');
  573. 573 : li = $(me.get_item(id, true));
  574. 574 : div = $(div).add(li.children().first());
  575. 575 : }
  576. 576 :
  577. 577 : if (p.id_decode)
  578. 578 : id = p.id_decode(id);
  579. 579 :
  580. 580 : div.toggleClass('subscribed');
  581. 581 : is_subscribed = div.hasClass('subscribed');
  582. 582 : $(this).attr('aria-checked', is_subscribed ? 'true' : 'false');
  583. 583 : me.triggerEvent('subscribe', { id: id, subscribed: is_subscribed, item: li });
  584. 584 :
  585. 585 : // update subscribe state of all 'virtual user' child folders
  586. 586 : if (li.hasClass('other user')) {
  587. 587 : $('ul li > div', li).each(function() {
  588. 588 : $(this)[is_subscribed ? 'addClass' : 'removeClass']('subscribed');
  589. 589 : $('.subscribed', div).attr('aria-checked', is_subscribed ? 'true' : 'false');
  590. 590 : });
  591. 591 : div.removeClass('partial');
  592. 592 : }
  593. 593 : // propagate subscription state to parent 'virtual user' folder
  594. 594 : else if (li.closest('li.other.user').length) {
  595. 595 : parent_subscription_status(li);
  596. 596 : }
  597. 597 :
  598. 598 : e.stopPropagation();
  599. 599 : return false;
  600. 600 : });
  601. 601 :
  602. 602 : this.container.on('click', 'a.remove', function(e) {
  603. 603 : var li = $(this).closest('li'),
  604. 604 : id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
  605. 605 :
  606. 606 : if (me.is_search()) {
  607. 607 : id = id.replace(/--xsR$/, '');
  608. 608 : li = $(me.get_item(id, true));
  609. 609 : }
  610. 610 :
  611. 611 : if (p.id_decode)
  612. 612 : id = p.id_decode(id);
  613. 613 :
  614. 614 : me.triggerEvent('remove', { id: id, item: li });
  615. 615 :
  616. 616 : e.stopPropagation();
  617. 617 : return false;
  618. 618 : });
  619. 619 : }
  620. 620 :
  621. 621 : // link prototype from base class
  622. 622 : if (window.rcube_treelist_widget) {
  623. 623 : kolab_folderlist.prototype = rcube_treelist_widget.prototype;
  624. 624 : }
  625. 625 :
  626. 626 :
  627. 627 : window.rcmail && rcmail.addEventListener('init', function(e) {
  628. 628 : var loading_lock;
  629. 629 :
  630. 630 : if (rcmail.env.task == 'mail') {
  631. 631 : rcmail.register_command('kolab-mail-history', function() {
  632. 632 : var dialog, uid = rcmail.get_single_uid(), rec = { uid: uid, mbox: rcmail.get_message_mailbox(uid) };
  633. 633 : if (!uid || !window.libkolab_audittrail) {
  634. 634 : return false;
  635. 635 : }
  636. 636 :
  637. 637 : // render dialog
  638. 638 : $dialog = libkolab_audittrail.object_history_dialog({
  639. 639 : module: 'libkolab',
  640. 640 : container: '#mailmessagehistory',
  641. 641 : title: rcmail.gettext('objectchangelog','libkolab')
  642. 642 : });
  643. 643 :
  644. 644 : $dialog.data('rec', rec);
  645. 645 :
  646. 646 : // fetch changelog data
  647. 647 : loading_lock = rcmail.set_busy(true, 'loading', loading_lock);
  648. 648 : rcmail.http_post('plugin.message-changelog', { _uid: rec.uid, _mbox: rec.mbox }, loading_lock);
  649. 649 :
  650. 650 : }, rcmail.env.action == 'show');
  651. 651 :
  652. 652 : rcmail.addEventListener('plugin.message_render_changelog', function(data) {
  653. 653 : var $dialog = $('#mailmessagehistory'),
  654. 654 : rec = $dialog.data('rec');
  655. 655 :
  656. 656 : if (data === false || !data.length || !rec) {
  657. 657 : // display 'unavailable' message
  658. 658 : $('<div class="notfound-message dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','libkolab') + '</div>')
  659. 659 : .insertBefore($dialog.find('.changelog-table').hide());
  660. 660 : return;
  661. 661 : }
  662. 662 :
  663. 663 : data.module = 'libkolab';
  664. 664 : libkolab_audittrail.render_changelog(data, rec, {});
  665. 665 : });
  666. 666 :
  667. 667 : rcmail.env.message_commands.push('kolab-mail-history');
  668. 668 : }
  669. 669 :
  670. 670 : if (rcmail.env.action == 'get-attachment') {
  671. 671 : if (rcmail.gui_objects.attachmentframe) {
  672. 672 : rcmail.gui_objects.messagepartframe = rcmail.gui_objects.attachmentframe;
  673. 673 : rcmail.enable_command('image-scale', 'image-rotate', !!/^image\//.test(rcmail.env.mimetype));
  674. 674 : rcmail.register_command('print-attachment', function() {
  675. 675 : var frame = rcmail.get_frame_window(rcmail.gui_objects.attachmentframe.id);
  676. 676 : if (frame) frame.print();
  677. 677 : }, true);
  678. 678 : }
  679. 679 :
  680. 680 : if (rcmail.env.attachment_download_url) {
  681. 681 : rcmail.register_command('download-attachment', function() {
  682. 682 : rcmail.location_href(rcmail.env.attachment_download_url, window);
  683. 683 : }, true);
  684. 684 : }
  685. 685 : }
  686. 686 : });