Thomas Bruederli
2014-06-03 4984078a56771a4f1be9af69eaedf8faefc2019b
commit | author | age
b34d67 1 /**
TB 2  * Roundcube List Widget
3  *
4  * This file is part of the Roundcube Webmail client
5  *
6  * @licstart  The following is the entire license notice for the
7  * JavaScript code in this file.
8  *
9  * Copyright (c) 2005-2014, The Roundcube Dev Team
10  *
11  * The JavaScript code in this page is free software: you can
12  * redistribute it and/or modify it under the terms of the GNU
13  * General Public License (GNU GPL) as published by the Free Software
14  * Foundation, either version 3 of the License, or (at your option)
15  * any later version.  The code is distributed WITHOUT ANY WARRANTY;
16  * without even the implied warranty of MERCHANTABILITY or FITNESS
17  * FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
18  *
19  * As additional permission under GNU GPL version 3 section 7, you
20  * may distribute non-source (e.g., minimized or compacted) forms of
21  * that code without the copy of the GNU GPL normally required by
22  * section 4, provided you include this license notice and a URL
23  * through which recipients can access the Corresponding Source.
24  *
25  * @licend  The above is the entire license notice
26  * for the JavaScript code in this file.
27  *
28  * @author Thomas Bruederli <roundcube@gmail.com>
29  * @author Charles McNulty <charles@charlesmcnulty.com>
30  *
31  * @requires jquery.js, common.js
32  */
6b47de 33
T 34
35 /**
e019f2 36  * Roundcube List Widget class
538e64 37  * @constructor
6b47de 38  */
T 39 function rcube_list_widget(list, p)
8fa922 40 {
6b47de 41   // static contants
T 42   this.ENTER_KEY = 13;
43   this.DELETE_KEY = 46;
6e6e89 44   this.BACKSPACE_KEY = 8;
8fa922 45
6b47de 46   this.list = list ? list : null;
517dae 47   this.tagname = this.list ? this.list.nodeName.toLowerCase() : 'table';
6a9144 48   this.id_regexp = /^rcmrow([a-z0-9\-_=\+\/]+)/i;
85fece 49   this.rows = {};
6b47de 50   this.selection = [];
0dbac3 51   this.rowcount = 0;
b62c48 52   this.colcount = 0;
8fa922 53
8fd955 54   this.subject_col = 0;
699a25 55   this.modkey = 0;
6b47de 56   this.multiselect = false;
f52c93 57   this.multiexpand = false;
21168d 58   this.multi_selecting = false;
6b47de 59   this.draggable = false;
b62c48 60   this.column_movable = false;
6b47de 61   this.keyboard = false;
68b6a9 62   this.toggleselect = false;
4582bf 63   this.aria_listbox = false;
8fa922 64
6b47de 65   this.drag_active = false;
b62c48 66   this.col_drag_active = false;
A 67   this.column_fixed = null;
6b47de 68   this.last_selected = 0;
b2fb95 69   this.shift_start = 0;
6b47de 70   this.focused = false;
T 71   this.drag_mouse_start = null;
c0e364 72   this.dblclick_time = 500; // default value on MS Windows is 500
517dae 73   this.row_init = function(){};  // @deprecated; use list.addEventListener('initrow') instead
8fa922 74
6b47de 75   // overwrite default paramaters
ef4f59 76   if (p && typeof p === 'object')
6b47de 77     for (var n in p)
T 78       this[n] = p[n];
8fa922 79 };
6b47de 80
T 81
82 rcube_list_widget.prototype = {
83
84
85 /**
86  * get all message rows from HTML table and init each row
87  */
88 init: function()
89 {
517dae 90   if (this.tagname == 'table' && this.list && this.list.tBodies[0]) {
TB 91     this.thead = this.list.tHead;
92     this.tbody = this.list.tBodies[0];
93   }
94   else if (this.tagname != 'table' && this.list) {
95     this.tbody = this.list;
96   }
97
498407 98   if ($(this.list).attr('role') == 'listbox') {
TB 99     this.aria_listbox = true;
100     if (this.multiselect)
101       $(this.list).attr('aria-multiselectable', 'true');
102   }
103
517dae 104   if (this.tbody) {
85fece 105     this.rows = {};
0dbac3 106     this.rowcount = 0;
6b47de 107
e8bcf0 108     var r, len, rows = this.tbody.childNodes, me = this;
6b47de 109
0e7b66 110     for (r=0, len=rows.length; r<len; r++) {
3ab616 111       this.rowcount += this.init_row(rows[r]) ? 1 : 0;
6b47de 112     }
T 113
b62c48 114     this.init_header();
6b47de 115     this.frame = this.list.parentNode;
T 116
117     // set body events
e8bcf0 118     if (this.keyboard) {
17a8fb 119       rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
e8bcf0 120
8fd955 121       // allow the table element to receive focus.
TB 122       $(this.list).attr('tabindex', '0')
123         .on('focus', function(e){ me.focus(e); });
e8bcf0 124     }
4582bf 125   }
TB 126
772bec 127   return this;
6b47de 128 },
T 129
130
131 /**
c4b819 132  * Init list row and set mouse events on it
6b47de 133  */
T 134 init_row: function(row)
135 {
6f1709 136   row.uid = this.get_row_uid(row);
1bbf8c 137
6b47de 138   // make references in internal array and set event handlers
1bbf8c 139   if (row && row.uid) {
TB 140     var self = this, uid = row.uid;
f52c93 141     this.rows[uid] = {uid:uid, id:row.id, obj:row};
6b47de 142
T 143     // set eventhandlers to table row
8ef2f3 144     row.onmousedown = function(e){ return self.drag_row(e, this.uid); };
T 145     row.onmouseup = function(e){ return self.click_row(e, this.uid); };
1ce442 146
4910b0 147     if (bw.touch) {
8ef2f3 148       row.addEventListener('touchstart', function(e) {
T 149         if (e.touches.length == 1) {
dc8400 150           self.touchmoved = false;
TB 151           self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid)
8ef2f3 152         }
T 153       }, false);
154       row.addEventListener('touchend', function(e) {
dc8400 155         if (e.changedTouches.length == 1) {
TB 156           if (!self.touchmoved && !self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
8ef2f3 157             e.preventDefault();
dc8400 158         }
TB 159       }, false);
160       row.addEventListener('touchmove', function(e) {
161         if (e.changedTouches.length == 1) {
162           self.touchmoved = true;
163           if (self.drag_active)
164             e.preventDefault();
165         }
8ef2f3 166       }, false);
T 167     }
6b47de 168
4582bf 169     // label the list row with the subject col as descriptive label
TB 170     if (this.aria_listbox) {
171       var lbl_id = 'l:' + row.id;
172       $(row)
173         .attr('role', 'option')
174         .attr('aria-labelledby', lbl_id)
175         .find(this.col_tagname()).eq(this.subject_col).attr('id', lbl_id);
176     }
177
6b47de 178     if (document.all)
T 179       row.onselectstart = function() { return false; };
180
517dae 181     this.row_init(this.rows[uid]);  // legacy support
TB 182     this.triggerEvent('initrow', this.rows[uid]);
3ab616 183
AM 184     return true;
b62c48 185   }
A 186 },
187
188
189 /**
190  * Init list column headers and set mouse events on them
191  */
192 init_header: function()
193 {
517dae 194   if (this.thead) {
b62c48 195     this.colcount = 0;
A 196
73ad4f 197     if (this.fixed_header) {  // copy (modified) fixed header back to the actual table
TB 198       $(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
93cd38 199       $(this.list.tHead).find('tr td').attr('style', '').find('a').attr('tabindex', '-1');  // remove fixed widths
73ad4f 200     }
93cd38 201     else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
73ad4f 202       this.init_fixed_header();
TB 203     }
204
b62c48 205     var col, r, p = this;
A 206     // add events for list columns moving
517dae 207     if (this.column_movable && this.thead && this.thead.rows) {
TB 208       for (r=0; r<this.thead.rows[0].cells.length; r++) {
b62c48 209         if (this.column_fixed == r)
A 210           continue;
517dae 211         col = this.thead.rows[0].cells[r];
b62c48 212         col.onmousedown = function(e){ return p.drag_column(e, this); };
A 213         this.colcount++;
214       }
215     }
6b47de 216   }
T 217 },
218
73ad4f 219 init_fixed_header: function()
TB 220 {
221   var clone = $(this.list.tHead).clone();
222
223   if (!this.fixed_header) {
224     this.fixed_header = $('<table>')
8efdd9 225       .attr('class', this.list.className + ' fixedcopy')
d4d62a 226       .attr('role', 'presentation')
73ad4f 227       .css({ position:'fixed' })
TB 228       .append(clone)
229       .append('<tbody></tbody>');
230     $(this.list).before(this.fixed_header);
231
232     var me = this;
233     $(window).resize(function(){ me.resize() });
4ae28f 234     $(window).scroll(function(){
TB 235       var w = $(window);
236       me.fixed_header.css('marginLeft', (-w.scrollLeft()) + 'px');
237       if (!bw.webkit)
238         me.fixed_header.css('marginTop', (-w.scrollTop()) + 'px');
239     });
73ad4f 240   }
TB 241   else {
242     $(this.fixed_header).find('thead').replaceWith(clone);
243   }
e8bcf0 244
TB 245   // avoid scrolling header links being focused
246   $(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
247
248   // set tabindex to fixed header sort links
249   clone.find('a.sortcol').attr('tabindex', '0');
73ad4f 250
TB 251   this.thead = clone.get(0);
252   this.resize();
253 },
254
255 resize: function()
256 {
257     if (!this.fixed_header)
258       return;
259
260     var column_widths = [];
261
262     // get column widths from original thead
263     $(this.tbody).parent().find('thead tr td').each(function(index) {
264       column_widths[index] = $(this).width();
265     });
266
267     // apply fixed widths to fixed table header
268     $(this.thead).parent().width($(this.tbody).parent().width());
269     $(this.thead).find('tr td').each(function(index) {
270       $(this).css('width', column_widths[index]);
271     });
cbd8f7 272
TB 273     $(window).scroll();
73ad4f 274 },
6b47de 275
T 276 /**
c4b819 277  * Remove all list rows
6b47de 278  */
f11541 279 clear: function(sel)
6b47de 280 {
517dae 281   if (this.tagname == 'table') {
TB 282     var tbody = document.createElement('tbody');
283     this.list.insertBefore(tbody, this.tbody);
284     this.list.removeChild(this.list.tBodies[1]);
285     this.tbody = tbody;
286   }
287   else {
288     $(this.row_tagname() + ':not(.thead)', this.tbody).remove();
289   }
54531f 290
85fece 291   this.rows = {};
0dbac3 292   this.rowcount = 0;
d58c39 293   this.last_selected = 0;
8fa922 294
A 295   if (sel)
296     this.clear_selection();
1633bc 297
A 298   // reset scroll position (in Opera)
299   if (this.frame)
300     this.frame.scrollTop = 0;
fd0c12 301
AM 302   // fix list header after removing any rows
303   this.resize();
6b47de 304 },
T 305
306
307 /**
308  * 'remove' message row from list (just hide it)
309  */
b62656 310 remove_row: function(uid, sel_next)
6b47de 311 {
fd0c12 312   var self = this, node = this.rows[uid] ? this.rows[uid].obj : null;
d741a9 313
517dae 314   if (!node)
d741a9 315     return;
A 316
517dae 317   node.style.display = 'none';
6b47de 318
b62656 319   if (sel_next)
T 320     this.select_next();
321
0e7b66 322   delete this.rows[uid];
0dbac3 323   this.rowcount--;
fd0c12 324
AM 325   // fix list header after removing any rows
326   clearTimeout(this.resize_timeout)
327   this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
6b47de 328 },
T 329
330
331 /**
c4b819 332  * Add row to the list and initialize it
6b47de 333  */
ea6d69 334 insert_row: function(row, before)
6b47de 335 {
fd0c12 336   var self = this, tbody = this.tbody;
6b47de 337
517dae 338   // create a real dom node first
TB 339   if (row.nodeName === undefined) {
340     // for performance reasons use DOM instead of jQuery here
341     var domrow = document.createElement(this.row_tagname());
342     if (row.id) domrow.id = row.id;
343     if (row.className) domrow.className = row.className;
344     if (row.style) $.extend(domrow.style, row.style);
1bbf8c 345     if (row.uid) $(domrow).data('uid', row.uid);
517dae 346
7a5c3a 347     for (var e, domcell, col, i=0; row.cols && i < row.cols.length; i++) {
517dae 348       col = row.cols[i];
TB 349       domcell = document.createElement(this.col_tagname());
350       if (col.className) domcell.className = col.className;
351       if (col.innerHTML) domcell.innerHTML = col.innerHTML;
7a5c3a 352       for (e in col.events)
AM 353         domcell['on' + e] = col.events[e];
517dae 354       domrow.appendChild(domcell);
TB 355     }
356
357     row = domrow;
358   }
359
a52297 360   if (before && tbody.childNodes.length)
ea6d69 361     tbody.insertBefore(row, (typeof before == 'object' && before.parentNode == tbody) ? before : tbody.firstChild);
6b47de 362   else
c4b819 363     tbody.appendChild(row);
6b47de 364
c4b819 365   this.init_row(row);
0dbac3 366   this.rowcount++;
fd0c12 367
AM 368   // fix list header after adding any rows
369   clearTimeout(this.resize_timeout)
370   this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
6b47de 371 },
T 372
517dae 373 /**
TB 374  * 
375  */
376 update_row: function(id, cols, newid, select)
377 {
378   var row = this.rows[id];
379   if (!row) return false;
380
381   var domrow = row.obj;
382   for (var domcell, col, i=0; cols && i < cols.length; i++) {
383     this.get_cell(domrow, i).html(cols[i]);
384   }
385
386   if (newid) {
387     delete this.rows[id];
388     domrow.id = 'rcmrow' + newid;
389     this.init_row(domrow);
390
391     if (select)
392       this.selection[0] = newid;
393   }
394 },
6b47de 395
T 396
397 /**
25c35c 398  * Set focus to the list
6b47de 399  */
T 400 focus: function(e)
401 {
e8bcf0 402   if (this.focused)
TB 403     return;
404
6b47de 405   this.focused = true;
T 406
e8bcf0 407   if (e)
TB 408     rcube_event.cancel(e);
409
8fd955 410   var focus_elem = null;
TB 411
412   if (this.last_selected && this.rows[this.last_selected]) {
413     focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0');
414   }
415
e26399 416   // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
8fd955 417   if (focus_elem && focus_elem.length) {
2ba491 418     // We now fix this by explicitly assigning focus to a dedicated link element
8fd955 419     this.focus_noscroll(focus_elem);
2ba491 420   }
TB 421   else {
422     // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
423     $('iframe,:focus:not(body)').blur();
424     window.focus();
425   }
2c2000 426
8fd955 427   $(this.list).addClass('focus').removeAttr('tabindex');
e8bcf0 428
TB 429   // set internal focus pointer to first row
430   if (!this.last_selected)
431     this.select_first(CONTROL_KEY);
6b47de 432 },
T 433
434
435 /**
436  * remove focus from the list
437  */
e8bcf0 438 blur: function(e)
6b47de 439 {
T 440   this.focused = false;
8fd955 441
TB 442   // avoid the table getting focus right again
443   var me = this;
444   setTimeout(function(){
445     $(me.list).removeClass('focus').attr('tabindex', '0');
446   }, 20);
447
448   if (this.last_selected && this.rows[this.last_selected]) {
449     $(this.rows[this.last_selected].obj)
450       .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
451   }
6b47de 452 },
T 453
93cd38 454 /**
TB 455  * Focus the given element without scrolling the list container
456  */
457 focus_noscroll: function(elem)
458 {
459   var y = this.frame.scrollTop || this.frame.scrollY;
460   elem.focus();
461   this.frame.scrollTop = y;
462 },
463
6b47de 464
T 465 /**
628706 466  * Set/unset the given column as hidden
T 467  */
468 hide_column: function(col, hide)
469 {
470   var method = hide ? 'addClass' : 'removeClass';
471
472   if (this.fixed_header)
473     $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
474
475   $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
6b47de 476 },
T 477
478
479 /**
b62c48 480  * onmousedown-handler of message list column
A 481  */
482 drag_column: function(e, col)
483 {
484   if (this.colcount > 1) {
485     this.drag_start = true;
486     this.drag_mouse_start = rcube_event.get_mouse_pos(e);
487
488     rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
489     rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
490
491     // enable dragging over iframes
492     this.add_dragfix();
493
494     // find selected column number
517dae 495     for (var i=0; i<this.thead.rows[0].cells.length; i++) {
TB 496       if (col == this.thead.rows[0].cells[i]) {
b62c48 497         this.selected_column = i;
A 498         break;
499       }
500     }
501   }
502
503   return false;
504 },
505
506
507 /**
6b47de 508  * onmousedown-handler of message list row
T 509  */
510 drag_row: function(e, id)
511 {
512   // don't do anything (another action processed before)
bc35e8 513   if (!this.is_event_target(e))
40d7c2 514     return true;
8fa922 515
f89f03 516   // accept right-clicks
T 517   if (rcube_event.get_button(e) == 2)
518     return true;
8fa922 519
052a6a 520   this.in_selection_before = e && e.istouch || this.in_selection(id) ? id : false;
AM 521
6b47de 522   // selects currently unselected row
052a6a 523   if (!this.in_selection_before) {
6b47de 524     var mod_key = rcube_event.get_modifier(e);
052a6a 525     this.select_row(id, mod_key, true);
6b47de 526   }
T 527
dc8400 528   if (this.draggable && this.selection.length && this.in_selection(id)) {
6b47de 529     this.drag_start = true;
b2fb95 530     this.drag_mouse_start = rcube_event.get_mouse_pos(e);
052a6a 531
6c11ee 532     rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
A 533     rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
4910b0 534     if (bw.touch) {
8ef2f3 535       rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
T 536       rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
537     }
cf6bc5 538
6c11ee 539     // enable dragging over iframes
b62c48 540     this.add_dragfix();
6b47de 541   }
T 542
543   return false;
544 },
545
546
547 /**
548  * onmouseup-handler of message list row
549  */
550 click_row: function(e, id)
551 {
bc35e8 552   // don't do anything (another action processed before)
AM 553   if (!this.is_event_target(e))
40d7c2 554     return true;
8fa922 555
bc35e8 556   var now = new Date().getTime(),
AM 557     dblclicked = now - this.rows[id].clicked < this.dblclick_time;
6b47de 558
052a6a 559   // unselects currently selected row
AM 560   if (!this.drag_active && !dblclicked && this.in_selection_before == id)
bc35e8 561     this.select_row(id, rcube_event.get_modifier(e), true);
89e507 562
6b47de 563   this.drag_start = false;
052a6a 564   this.in_selection_before = false;
6b47de 565
T 566   // row was double clicked
85fece 567   if (this.rowcount && dblclicked && this.in_selection(id)) {
cc97ea 568     this.triggerEvent('dblclick');
fc643e 569     now = 0;
T 570   }
6b47de 571   else
cc97ea 572     this.triggerEvent('click');
6b47de 573
dd51b7 574   if (!this.drag_active) {
A 575     // remove temp divs
b62c48 576     this.del_dragfix();
6b47de 577     rcube_event.cancel(e);
dd51b7 578   }
6b47de 579
T 580   this.rows[id].clicked = now;
ea0866 581   this.focus();
TB 582
6b47de 583   return false;
T 584 },
585
586
bc35e8 587 /**
AM 588  * Check target of the current event
589  */
590 is_event_target: function(e)
591 {
592   var target = rcube_event.get_target(e),
593     tagname = target.tagName.toLowerCase();
594
595   return !(target && (tagname == 'input' || tagname == 'img' || (tagname != 'a' && target.onclick)));
596 },
597
598
bc2acc 599 /*
A 600  * Returns thread root ID for specified row ID
601  */
602 find_root: function(uid)
603 {
604    var r = this.rows[uid];
605
606    if (r && r.parent_uid)
607      return this.find_root(r.parent_uid);
608    else
609      return uid;
610 },
611
612
f52c93 613 expand_row: function(e, id)
T 614 {
54531f 615   var row = this.rows[id],
A 616     evtarget = rcube_event.get_target(e),
617     mod_key = rcube_event.get_modifier(e);
f52c93 618
T 619   // Don't treat double click on the expando as double click on the message.
620   row.clicked = 0;
621
622   if (row.expanded) {
54531f 623     evtarget.className = 'collapsed';
f52c93 624     if (mod_key == CONTROL_KEY || this.multiexpand)
T 625       this.collapse_all(row);
626     else
627       this.collapse(row);
628   }
629   else {
54531f 630     evtarget.className = 'expanded';
f52c93 631     if (mod_key == CONTROL_KEY || this.multiexpand)
T 632       this.expand_all(row);
633     else
634      this.expand(row);
635   }
636 },
637
638 collapse: function(row)
639 {
7eecf8 640   var r, depth = row.depth,
AM 641     new_row = row ? row.obj.nextSibling : null;
642
f52c93 643   row.expanded = false;
32afef 644   this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
f52c93 645
T 646   while (new_row) {
647     if (new_row.nodeType == 1) {
7eecf8 648       r = this.rows[new_row.uid];
f52c93 649       if (r && r.depth <= depth)
T 650         break;
7eecf8 651
a3c9bd 652       $(new_row).css('display', 'none');
54531f 653       if (r.expanded) {
A 654         r.expanded = false;
32afef 655         this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
54531f 656       }
f52c93 657     }
T 658     new_row = new_row.nextSibling;
659   }
660
73ad4f 661   this.resize();
d94a71 662   this.triggerEvent('listupdate');
7eecf8 663
f52c93 664   return false;
T 665 },
666
667 expand: function(row)
668 {
1633bc 669   var r, p, depth, new_row, last_expanded_parent_depth;
f52c93 670
T 671   if (row) {
672     row.expanded = true;
673     depth = row.depth;
674     new_row = row.obj.nextSibling;
1bbf8c 675     this.update_expando(row.id, true);
32afef 676     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
f52c93 677   }
T 678   else {
517dae 679     var tbody = this.tbody;
f52c93 680     new_row = tbody.firstChild;
T 681     depth = 0;
682     last_expanded_parent_depth = 0;
683   }
684
685   while (new_row) {
686     if (new_row.nodeType == 1) {
1633bc 687       r = this.rows[new_row.uid];
f52c93 688       if (r) {
T 689         if (row && (!r.depth || r.depth <= depth))
690           break;
691
692         if (r.parent_uid) {
1633bc 693           p = this.rows[r.parent_uid];
f52c93 694           if (p && p.expanded) {
T 695             if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
696               last_expanded_parent_depth = p.depth;
a3c9bd 697               $(new_row).css('display', '');
f52c93 698               r.expanded = true;
32afef 699               this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
f52c93 700             }
T 701           }
702           else
703             if (row && (! p || p.depth <= depth))
704               break;
705         }
706       }
707     }
708     new_row = new_row.nextSibling;
709   }
710
73ad4f 711   this.resize();
d94a71 712   this.triggerEvent('listupdate');
f52c93 713   return false;
T 714 },
715
716
717 collapse_all: function(row)
718 {
54531f 719   var depth, new_row, r;
f52c93 720
T 721   if (row) {
722     row.expanded = false;
723     depth = row.depth;
724     new_row = row.obj.nextSibling;
1bbf8c 725     this.update_expando(row.id);
32afef 726     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
8fa922 727
f52c93 728     // don't collapse sub-root tree in multiexpand mode 
T 729     if (depth && this.multiexpand)
54531f 730       return false;
f52c93 731   }
T 732   else {
517dae 733     new_row = this.tbody.firstChild;
f52c93 734     depth = 0;
T 735   }
736
737   while (new_row) {
738     if (new_row.nodeType == 1) {
54531f 739       if (r = this.rows[new_row.uid]) {
f52c93 740         if (row && (!r.depth || r.depth <= depth))
T 741           break;
742
743         if (row || r.depth)
a3c9bd 744           $(new_row).css('display', 'none');
54531f 745         if (r.has_children && r.expanded) {
f52c93 746           r.expanded = false;
1bbf8c 747           this.update_expando(r.id, false);
32afef 748           this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
f52c93 749         }
T 750       }
751     }
752     new_row = new_row.nextSibling;
753   }
754
73ad4f 755   this.resize();
d94a71 756   this.triggerEvent('listupdate');
f52c93 757   return false;
T 758 },
759
2b55d4 760
f52c93 761 expand_all: function(row)
T 762 {
54531f 763   var depth, new_row, r;
f52c93 764
T 765   if (row) {
766     row.expanded = true;
767     depth = row.depth;
768     new_row = row.obj.nextSibling;
1bbf8c 769     this.update_expando(row.id, true);
32afef 770     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
f52c93 771   }
T 772   else {
517dae 773     new_row = this.tbody.firstChild;
f52c93 774     depth = 0;
T 775   }
776
777   while (new_row) {
778     if (new_row.nodeType == 1) {
54531f 779       if (r = this.rows[new_row.uid]) {
f52c93 780         if (row && r.depth <= depth)
T 781           break;
782
a3c9bd 783         $(new_row).css('display', '');
54531f 784         if (r.has_children && !r.expanded) {
f52c93 785           r.expanded = true;
1bbf8c 786           this.update_expando(r.id, true);
32afef 787           this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
f52c93 788         }
T 789       }
790     }
791     new_row = new_row.nextSibling;
792   }
d94a71 793
73ad4f 794   this.resize();
d94a71 795   this.triggerEvent('listupdate');
f52c93 796   return false;
T 797 },
2b55d4 798
bc2acc 799
1bbf8c 800 update_expando: function(id, expanded)
bc2acc 801 {
1bbf8c 802   var expando = document.getElementById('rcmexpando' + id);
bc2acc 803   if (expando)
A 804     expando.className = expanded ? 'expanded' : 'collapsed';
805 },
806
6f1709 807 get_row_uid: function(row)
TB 808 {
809   if (row && row.uid)
810     return row.uid;
811
812   var uid;
813   if (row && (uid = $(row).data('uid')))
814     row.uid = uid;
815   else if (row && String(row.id).match(this.id_regexp))
816     row.uid = RegExp.$1;
817
818   return row.uid;
819 },
f52c93 820
6b47de 821 /**
49771b 822  * get first/next/previous/last rows that are not hidden
6b47de 823  */
T 824 get_next_row: function()
825 {
85fece 826   if (!this.rowcount)
6b47de 827     return false;
T 828
54531f 829   var last_selected_row = this.rows[this.last_selected],
A 830     new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
831
6b47de 832   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
T 833     new_row = new_row.nextSibling;
834
835   return new_row;
836 },
837
838 get_prev_row: function()
839 {
85fece 840   if (!this.rowcount)
6b47de 841     return false;
T 842
54531f 843   var last_selected_row = this.rows[this.last_selected],
A 844     new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
845
6b47de 846   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
T 847     new_row = new_row.previousSibling;
848
849   return new_row;
49771b 850 },
A 851
852 get_first_row: function()
853 {
8fa922 854   if (this.rowcount) {
6f1709 855     var i, len, uid, rows = this.tbody.childNodes;
49771b 856
0e7b66 857     for (i=0, len=rows.length-1; i<len; i++)
6f1709 858       if (rows[i].id && (uid = this.get_row_uid(rows[i])))
TB 859         return uid;
8fa922 860   }
49771b 861
A 862   return null;
6b47de 863 },
T 864
095d05 865 get_last_row: function()
A 866 {
8fa922 867   if (this.rowcount) {
6f1709 868     var i, uid, rows = this.tbody.childNodes;
6b47de 869
0e7b66 870     for (i=rows.length-1; i>=0; i--)
6f1709 871       if (rows[i].id && (uid = this.get_row_uid(rows[i])))
TB 872         return uid;
8fa922 873   }
095d05 874
A 875   return null;
876 },
877
517dae 878 row_tagname: function()
TB 879 {
880   var row_tagnames = { table:'tr', ul:'li', '*':'div' };
881   return row_tagnames[this.tagname] || row_tagnames['*'];
882 },
883
884 col_tagname: function()
885 {
886   var col_tagnames = { table:'td', '*':'span' };
887   return col_tagnames[this.tagname] || col_tagnames['*'];
888 },
889
890 get_cell: function(row, index)
891 {
892   return $(this.col_tagname(), row).eq(index);
893 },
095d05 894
A 895 /**
896  * selects or unselects the proper row depending on the modifier key pressed
897  */
6b47de 898 select_row: function(id, mod_key, with_mouse)
T 899 {
498407 900   var select_before = this.selection.join(','),
TB 901     in_selection_before = this.in_selection(id);
052a6a 902
498407 903   if (!this.multiselect && with_mouse)
6b47de 904     mod_key = 0;
8fa922 905
b2fb95 906   if (!this.shift_start)
T 907     this.shift_start = id
6b47de 908
8fa922 909   if (!mod_key) {
6b47de 910     this.shift_start = id;
T 911     this.highlight_row(id, false);
21168d 912     this.multi_selecting = false;
6b47de 913   }
8fa922 914   else {
A 915     switch (mod_key) {
6b47de 916       case SHIFT_KEY:
b2fb95 917         this.shift_select(id, false);
6b47de 918         break;
T 919
920       case CONTROL_KEY:
e769a7 921         if (with_mouse) {
C 922           this.shift_start = id;
b2fb95 923           this.highlight_row(id, true);
e769a7 924         }
699a25 925         break;
6b47de 926
T 927       case CONTROL_SHIFT_KEY:
928         this.shift_select(id, true);
929         break;
930
931       default:
b2fb95 932         this.highlight_row(id, false);
6b47de 933         break;
T 934     }
052a6a 935
21168d 936     this.multi_selecting = true;
6b47de 937   }
T 938
8fd955 939   if (this.last_selected != 0 && this.rows[this.last_selected]) {
TB 940     $(this.rows[this.last_selected].obj).removeClass('focused')
941       .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
942   }
a9dda5 943
T 944   // unselect if toggleselect is active and the same row was clicked again
498407 945   if (this.toggleselect && in_selection_before) {
a9dda5 946     this.clear_selection();
T 947   }
498407 948   // trigger event if selection changed
TB 949   else if (this.selection.join(',') != select_before) {
950     this.triggerEvent('select');
951   }
952
953   if (this.rows[id]) {
cc97ea 954     $(this.rows[id].obj).addClass('focused');
8fd955 955     // set cursor focus to link inside selected row
TB 956     if (this.focused)
957       this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0'));
958   }
a9dda5 959
b2fb95 960   if (!this.selection.length)
T 961     this.shift_start = null;
6b47de 962
T 963   this.last_selected = id;
964 },
965
966
967 /**
968  * Alias method for select_row
969  */
970 select: function(id)
971 {
972   this.select_row(id, false);
973   this.scrollto(id);
974 },
975
976
977 /**
978  * Select row next to the last selected one.
979  * Either below or above.
980  */
981 select_next: function()
982 {
1633bc 983   var next_row = this.get_next_row(),
A 984     prev_row = this.get_prev_row(),
985     new_row = (next_row) ? next_row : prev_row;
986
6b47de 987   if (new_row)
1ce442 988     this.select_row(new_row.uid, false, false);
6b47de 989 },
T 990
bc2acc 991
49771b 992 /**
A 993  * Select first row 
994  */
bc2acc 995 select_first: function(mod_key)
49771b 996 {
bc2acc 997   var row = this.get_first_row();
1633bc 998   if (row) {
03da10 999     this.select_row(row, mod_key, false);
AM 1000     this.scrollto(row);
bc2acc 1001   }
49771b 1002 },
bc2acc 1003
A 1004
1005 /**
2b55d4 1006  * Select last row
bc2acc 1007  */
A 1008 select_last: function(mod_key)
1009 {
1010   var row = this.get_last_row();
1633bc 1011   if (row) {
03da10 1012     this.select_row(row, mod_key, false);
AM 1013     this.scrollto(row);
bc2acc 1014   }
A 1015 },
1016
49771b 1017
84a331 1018 /**
T 1019  * Add all childs of the given row to selection
1020  */
2b55d4 1021 select_children: function(uid)
84a331 1022 {
2b55d4 1023   var i, children = this.row_children(uid), len = children.length;
8fa922 1024
2b55d4 1025   for (i=0; i<len; i++)
AM 1026     if (!this.in_selection(children[i]))
5c7bbf 1027       this.select_row(children[i], CONTROL_KEY, true);
84a331 1028 },
T 1029
6b47de 1030
T 1031 /**
1032  * Perform selection when shift key is pressed
1033  */
1034 shift_select: function(id, control)
1035 {
b00bd0 1036   if (!this.rows[this.shift_start] || !this.selection.length)
f2892d 1037     this.shift_start = id;
A 1038
50cc5b 1039   var n, i, j, to_row = this.rows[id],
517dae 1040     from_rowIndex = this._rowIndex(this.rows[this.shift_start].obj),
TB 1041     to_rowIndex = this._rowIndex(to_row.obj);
50cc5b 1042
d19417 1043   // if we're going down the list, and we hit a thread, and it's closed, select the whole thread
CM 1044   if (from_rowIndex < to_rowIndex && !to_row.expanded && to_row.has_children)
50cc5b 1045     if (to_row = this.rows[(this.row_children(id)).pop()])
517dae 1046       to_rowIndex = this._rowIndex(to_row.obj);
50cc5b 1047
AM 1048   i = ((from_rowIndex < to_rowIndex) ? from_rowIndex : to_rowIndex),
1049   j = ((from_rowIndex > to_rowIndex) ? from_rowIndex : to_rowIndex);
6b47de 1050
T 1051   // iterate through the entire message list
1633bc 1052   for (n in this.rows) {
517dae 1053     if (this._rowIndex(this.rows[n].obj) >= i && this._rowIndex(this.rows[n].obj) <= j) {
f52c93 1054       if (!this.in_selection(n)) {
6b47de 1055         this.highlight_row(n, true);
f52c93 1056       }
6b47de 1057     }
8fa922 1058     else {
1633bc 1059       if (this.in_selection(n) && !control) {
6b47de 1060         this.highlight_row(n, true);
f52c93 1061       }
6b47de 1062     }
T 1063   }
1064 },
1065
d19417 1066
517dae 1067 /**
TB 1068  * Helper method to emulate the rowIndex property of non-tr elements
1069  */
1070 _rowIndex: function(obj)
1071 {
1072   return (obj.rowIndex !== undefined) ? obj.rowIndex : $(obj).prevAll().length;
1073 },
6b47de 1074
T 1075 /**
1076  * Check if given id is part of the current selection
1077  */
1078 in_selection: function(id)
1079 {
1633bc 1080   for (var n in this.selection)
7eecf8 1081     if (this.selection[n] == id)
6b47de 1082       return true;
T 1083
f52c93 1084   return false;
6b47de 1085 },
T 1086
1087
1088 /**
1089  * Select each row in list
1090  */
1091 select_all: function(filter)
1092 {
85fece 1093   if (!this.rowcount)
6b47de 1094     return false;
T 1095
1094cd 1096   // reset but remember selection first
1633bc 1097   var n, select_before = this.selection.join(',');
8fa922 1098   this.selection = [];
A 1099
1633bc 1100   for (n in this.rows) {
0e7b66 1101     if (!filter || this.rows[n][filter] == true) {
6b47de 1102       this.last_selected = n;
ad827b 1103       this.highlight_row(n, true, true);
6b47de 1104     }
0e7b66 1105     else {
1791a1 1106       $(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');
874717 1107     }
6b47de 1108   }
T 1109
1094cd 1110   // trigger event if selection changed
T 1111   if (this.selection.join(',') != select_before)
cc97ea 1112     this.triggerEvent('select');
1094cd 1113
d7c226 1114   this.focus();
A 1115
1094cd 1116   return true;
6b47de 1117 },
T 1118
1119
1120 /**
528185 1121  * Invert selection
A 1122  */
1123 invert_selection: function()
1124 {
85fece 1125   if (!this.rowcount)
528185 1126     return false;
A 1127
1128   // remember old selection
1633bc 1129   var n, select_before = this.selection.join(',');
8fa922 1130
1633bc 1131   for (n in this.rows)
f52c93 1132     this.highlight_row(n, true);
528185 1133
A 1134   // trigger event if selection changed
1135   if (this.selection.join(',') != select_before)
1136     this.triggerEvent('select');
1137
1138   this.focus();
1139
1140   return true;
1141 },
1142
1143
1144 /**
095d05 1145  * Unselect selected row(s)
6b47de 1146  */
688fd7 1147 clear_selection: function(id, no_event)
6b47de 1148 {
1633bc 1149   var n, num_select = this.selection.length;
095d05 1150
A 1151   // one row
8fa922 1152   if (id) {
1633bc 1153     for (n in this.selection)
cc97ea 1154       if (this.selection[n] == id) {
T 1155         this.selection.splice(n,1);
1156         break;
1157       }
8fa922 1158   }
095d05 1159   // all rows
8fa922 1160   else {
1633bc 1161     for (n in this.selection)
cc97ea 1162       if (this.rows[this.selection[n]]) {
1791a1 1163         $(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');
8fa922 1164       }
A 1165
1166     this.selection = [];
1167   }
6b47de 1168
e8bcf0 1169   if (num_select && !this.selection.length && !no_event) {
cc97ea 1170     this.triggerEvent('select');
e8bcf0 1171     this.last_selected = 0;
TB 1172   }
6b47de 1173 },
T 1174
1175
1176 /**
1177  * Getter for the selection array
1178  */
7eecf8 1179 get_selection: function(deep)
6b47de 1180 {
7eecf8 1181   var res = $.merge([], this.selection);
AM 1182
1183   // return children of selected threads even if only root is selected
1184   if (deep !== false && res.length) {
1185     for (var uid, uids, i=0, len=res.length; i<len; i++) {
1186       uid = res[i];
f67037 1187       if (this.rows[uid] && this.rows[uid].has_children && !this.rows[uid].expanded) {
7eecf8 1188         uids = this.row_children(uid);
AM 1189         for (var j=0, uids_len=uids.length; j<uids_len; j++) {
1190           uid = uids[j];
1191           if (!this.in_selection(uid))
1192             res.push(uid);
1193         }
1194       }
1195     }
1196   }
1197
1198   return res;
6b47de 1199 },
T 1200
1201
1202 /**
1203  * Return the ID if only one row is selected
1204  */
1205 get_single_selection: function()
1206 {
1207   if (this.selection.length == 1)
1208     return this.selection[0];
1209   else
1210     return null;
1211 },
1212
1213
1214 /**
1215  * Highlight/unhighlight a row
1216  */
ad827b 1217 highlight_row: function(id, multiple, norecur)
6b47de 1218 {
2b55d4 1219   if (!this.rows[id])
AM 1220     return;
1221
1222   if (!multiple) {
8fa922 1223     if (this.selection.length > 1 || !this.in_selection(id)) {
688fd7 1224       this.clear_selection(null, true);
df015d 1225       this.selection[0] = id;
a2f8fa 1226       $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
df015d 1227     }
6b47de 1228   }
2b55d4 1229   else {
8fa922 1230     if (!this.in_selection(id)) { // select row
2b55d4 1231       this.selection.push(id);
a2f8fa 1232       $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
ad827b 1233       if (!norecur && !this.rows[id].expanded)
2b55d4 1234         this.highlight_children(id, true);
6b47de 1235     }
8fa922 1236     else { // unselect row
1633bc 1237       var p = $.inArray(id, this.selection),
A 1238         a_pre = this.selection.slice(0, p),
1239         a_post = this.selection.slice(p+1, this.selection.length);
1240
6b47de 1241       this.selection = a_pre.concat(a_post);
1791a1 1242       $(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
ad827b 1243       if (!norecur && !this.rows[id].expanded)
2b55d4 1244         this.highlight_children(id, false);
6b47de 1245     }
2b55d4 1246   }
AM 1247 },
1248
1249
1250 /**
1251  * Highlight/unhighlight all childs of the given row
1252  */
1253 highlight_children: function(id, status)
1254 {
1255   var i, selected,
1256     children = this.row_children(id), len = children.length;
1257
1258   for (i=0; i<len; i++) {
1259     selected = this.in_selection(children[i]);
1260     if ((status && !selected) || (!status && selected))
ad827b 1261       this.highlight_row(children[i], true, true);
6b47de 1262   }
T 1263 },
1264
1265
1266 /**
1267  * Handler for keyboard events
1268  */
1269 key_press: function(e)
1270 {
ebee2a 1271   var target = e.target || {};
808055 1272
ebee2a 1273   if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
6b47de 1274     return true;
T 1275
1633bc 1276   var keyCode = rcube_event.get_keycode(e),
A 1277     mod_key = rcube_event.get_modifier(e);
91d1a1 1278
8fa922 1279   switch (keyCode) {
6b47de 1280     case 40:
699a25 1281     case 38:
26f5b0 1282     case 63233: // "down", in safari keypress
T 1283     case 63232: // "up", in safari keypress
1284       // Stop propagation so that the browser doesn't scroll
1285       rcube_event.cancel(e);
6b47de 1286       return this.use_arrow_key(keyCode, mod_key);
5c7bbf 1287
f52c93 1288     case 32:
9806c7 1289       rcube_event.cancel(e);
C 1290       return this.select_row(this.last_selected, mod_key, true);
5c7bbf 1291
9806c7 1292     case 37: // Left arrow key
C 1293     case 39: // Right arrow key
f52c93 1294       // Stop propagation
T 1295       rcube_event.cancel(e);
808055 1296       var ret = this.use_arrow_key(keyCode, mod_key);
f52c93 1297       this.key_pressed = keyCode;
699a25 1298       this.modkey = mod_key;
f52c93 1299       this.triggerEvent('keypress');
699a25 1300       this.modkey = 0;
f52c93 1301       return ret;
5c7bbf 1302
bc2acc 1303     case 36: // Home
A 1304       this.select_first(mod_key);
1305       return rcube_event.cancel(e);
5c7bbf 1306
bc2acc 1307     case 35: // End
A 1308       this.select_last(mod_key);
1309       return rcube_event.cancel(e);
5c7bbf 1310
17a8fb 1311     case 27:
AM 1312       if (this.drag_active)
1313         return this.drag_mouse_up(e);
5c7bbf 1314
17a8fb 1315       if (this.col_drag_active) {
AM 1316         this.selected_column = null;
1317         return this.column_drag_mouse_up(e);
1318       }
5c7bbf 1319
17a8fb 1320       return rcube_event.cancel(e);
5c7bbf 1321
8fd955 1322     case 9: // Tab
TB 1323       this.blur();
1324       break;
1325
1326     case 13: // Enter
1327       if (!this.selection.length)
1328         this.select_row(this.last_selected, mod_key, false);
1329
6b47de 1330     default:
T 1331       this.key_pressed = keyCode;
699a25 1332       this.modkey = mod_key;
cc97ea 1333       this.triggerEvent('keypress');
699a25 1334       this.modkey = 0;
8fa922 1335
f89f03 1336       if (this.key_pressed == this.BACKSPACE_KEY)
6e6e89 1337         return rcube_event.cancel(e);
9e7a1b 1338   }
8fa922 1339
9e7a1b 1340   return true;
T 1341 },
1342
6b47de 1343
T 1344 /**
1345  * Special handling method for arrow keys
1346  */
1347 use_arrow_key: function(keyCode, mod_key)
1348 {
808055 1349   var new_row,
AM 1350     selected_row = this.rows[this.last_selected];
1351
e9b57b 1352   // Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
A 1353   // using the keypress event (but not the keydown or keyup event).
1354   if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
6b47de 1355     new_row = this.get_next_row();
e9b57b 1356   else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
6b47de 1357     new_row = this.get_prev_row();
808055 1358   else {
AM 1359     if (!selected_row || !selected_row.has_children)
1360       return;
1361
1362     // expand
1363     if (keyCode == 39) {
1364       if (selected_row.expanded)
1365         return;
1366
1367       if (mod_key == CONTROL_KEY || this.multiexpand)
1368         this.expand_all(selected_row);
1369       else
1370         this.expand(selected_row);
1371     }
1372     // collapse
1373     else {
1374       if (!selected_row.expanded)
1375         return;
1376
1377       if (mod_key == CONTROL_KEY || this.multiexpand)
1378         this.collapse_all(selected_row);
1379       else
1380         this.collapse(selected_row);
1381     }
1382
1bbf8c 1383     this.update_expando(selected_row.id, selected_row.expanded);
808055 1384
AM 1385     return false;
1386   }
6b47de 1387
8fa922 1388   if (new_row) {
e8bcf0 1389     // simulate ctr-key if no rows are selected
TB 1390     if (!mod_key && !this.selection.length)
1391       mod_key = CONTROL_KEY;
1392
699a25 1393     this.select_row(new_row.uid, mod_key, false);
6b47de 1394     this.scrollto(new_row.uid);
T 1395   }
e8bcf0 1396   else if (!new_row && !selected_row) {
TB 1397     // select the first row if none selected yet
1398     this.select_first(CONTROL_KEY);
1399   }
6b47de 1400
T 1401   return false;
1402 },
1403
1404
1405 /**
1406  * Try to scroll the list to make the specified row visible
1407  */
1408 scrollto: function(id)
1409 {
1410   var row = this.rows[id].obj;
8fa922 1411   if (row && this.frame) {
741f38 1412     var scroll_to = Number(row.offsetTop),
C 1413       head_offset = 0;
6b47de 1414
bc2acc 1415     // expand thread if target row is hidden (collapsed)
A 1416     if (!scroll_to && this.rows[id].parent_uid) {
1417       var parent = this.find_root(this.rows[id].uid);
1418       this.expand_all(this.rows[parent]);
1419       scroll_to = Number(row.offsetTop);
1420     }
1421
8f8e26 1422     if (this.fixed_header)
741f38 1423       head_offset = Number(this.thead.offsetHeight);
8f8e26 1424
741f38 1425     // if row is above the frame (or behind header)
C 1426     if (scroll_to < Number(this.frame.scrollTop) + head_offset) {
1427       // scroll window so that row isn't behind header
1428       this.frame.scrollTop = scroll_to - head_offset;
8f8e26 1429     }
AM 1430     else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
6b47de 1431       this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
T 1432   }
1433 },
1434
1435
1436 /**
1437  * Handler for mouse move events
1438  */
1439 drag_mouse_move: function(e)
1440 {
8ef2f3 1441   // convert touch event
T 1442   if (e.type == 'touchmove') {
dc8400 1443     if (e.touches.length == 1 && e.changedTouches.length == 1)
8ef2f3 1444       e = rcube_event.touchevent(e.changedTouches[0]);
T 1445     else
1446       return rcube_event.cancel(e);
1447   }
2b55d4 1448
8fa922 1449   if (this.drag_start) {
6b47de 1450     // check mouse movement, of less than 3 pixels, don't start dragging
f89637 1451     var m = rcube_event.get_mouse_pos(e),
AM 1452       limit = 10, selection = [], self = this;
cf6bc5 1453
6b47de 1454     if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
T 1455       return false;
8fa922 1456
f89637 1457     // remember dragging start position
AM 1458     this.drag_start_pos = {left: m.x, top: m.y};
1459
1460     // initialize drag layer
6b47de 1461     if (!this.draglayer)
b62c48 1462       this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
f89637 1463         .css({position: 'absolute', display: 'none', 'z-index': 2000})
b62c48 1464         .appendTo(document.body);
f89637 1465     else
AM 1466       this.draglayer.html('');
8fa922 1467
f89637 1468     // get selected rows (in display order), don't use this.selection here
AM 1469     $(this.row_tagname() + '.selected', this.tbody).each(function() {
6f1709 1470       var uid = self.get_row_uid(this), row = self.rows[uid];
74cd6c 1471
c83a95 1472       if (!row || $.inArray(uid, selection) > -1)
f89637 1473         return;
6b47de 1474
f89637 1475       selection.push(uid);
f52c93 1476
f89637 1477       // also handle children of (collapsed) trees for dragging (they might be not selected)
AM 1478       if (row.has_children && !row.expanded)
1479         $.each(self.row_children(uid), function() {
1480           if ($.inArray(this, selection) > -1)
1481             return;
1482           selection.push(this);
517dae 1483         });
f89637 1484
AM 1485       // break the loop asap
1486       if (selection.length > limit + 1)
1487         return false;
1488     });
1489
1490     // append subject (of every row up to the limit) to the drag layer
1491     $.each(selection, function(i, uid) {
1492       if (i > limit) {
1493         self.draglayer.append('...');
1494         return false;
6b47de 1495       }
f89637 1496
AM 1497       $('> ' + self.col_tagname(), self.rows[uid].obj).each(function(n, cell) {
1498         if (self.subject_col < 0 || (self.subject_col >= 0 && self.subject_col == n)) {
1499           var subject = $(cell).text();
1500
1501           if (subject) {
1502             // remove leading spaces
1503             subject = $.trim(subject);
1504             // truncate line to 50 characters
1505             subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
1506
1507             self.draglayer.append($('<div>').text(subject));
1508             return false;
1509           }
1510         }
1511       });
1512     });
6b47de 1513
cc97ea 1514     this.draglayer.show();
6b47de 1515     this.drag_active = true;
cc97ea 1516     this.triggerEvent('dragstart');
6b47de 1517   }
T 1518
8fa922 1519   if (this.drag_active && this.draglayer) {
6b47de 1520     var pos = rcube_event.get_mouse_pos(e);
cc97ea 1521     this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
9489ad 1522     this.triggerEvent('dragmove', e?e:window.event);
6b47de 1523   }
T 1524
1525   this.drag_start = false;
1526
1527   return false;
1528 },
1529
1530
1531 /**
1532  * Handler for mouse up events
1533  */
1534 drag_mouse_up: function(e)
1535 {
1536   document.onmousemove = null;
1257dd 1537
8ef2f3 1538   if (e.type == 'touchend') {
T 1539     if (e.changedTouches.length != 1)
1540       return rcube_event.cancel(e);
1541   }
6b47de 1542
cc97ea 1543   if (this.draglayer && this.draglayer.is(':visible')) {
T 1544     if (this.drag_start_pos)
1545       this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
1546     else
1547       this.draglayer.hide();
1548   }
6b47de 1549
da8f11 1550   if (this.drag_active)
A 1551     this.focus();
6b47de 1552   this.drag_active = false;
6c11ee 1553
A 1554   rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
1555   rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
1257dd 1556
4910b0 1557   if (bw.touch) {
8ef2f3 1558     rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
T 1559     rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
1560   }
6c11ee 1561
A 1562   // remove temp divs
b62c48 1563   this.del_dragfix();
6c11ee 1564
76a98d 1565   this.triggerEvent('dragend', e);
da8f11 1566
6b47de 1567   return rcube_event.cancel(e);
c4b819 1568 },
A 1569
1570
1571 /**
b62c48 1572  * Handler for mouse move events for dragging list column
A 1573  */
1574 column_drag_mouse_move: function(e)
1575 {
1576   if (this.drag_start) {
1577     // check mouse movement, of less than 3 pixels, don't start dragging
1578     var i, m = rcube_event.get_mouse_pos(e);
1579
1580     if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
1581       return false;
1582
1583     if (!this.col_draglayer) {
1584       var lpos = $(this.list).offset(),
517dae 1585         cells = this.thead.rows[0].cells;
b62c48 1586
0c8049 1587       // fix layer position when list is scrolled
AM 1588       lpos.top += this.list.scrollTop + this.list.parentNode.scrollTop;
1589
b62c48 1590       // create dragging layer
A 1591       this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
1592         .css(lpos).css({ position:'absolute', 'z-index':2001,
1593            'background-color':'white', opacity:0.75,
1594            height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
1595         .appendTo(document.body)
1596         // ... and column position indicator
1597        .append($('<div>').attr('id', 'rcmcolumnindicator')
1598           .css({ position:'absolute', 'border-right':'2px dotted #555', 
1599           'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
1600
1601       this.cols = [];
1602       this.list_pos = this.list_min_pos = lpos.left;
1603       // save columns positions
1604       for (i=0; i<cells.length; i++) {
1605         this.cols[i] = cells[i].offsetWidth;
1606         if (this.column_fixed !== null && i <= this.column_fixed) {
1607           this.list_min_pos += this.cols[i];
1608         }
1609       }
1610     }
1611
1612     this.col_draglayer.show();
1613     this.col_drag_active = true;
1614     this.triggerEvent('column_dragstart');
1615   }
1616
1617   // set column indicator position
1618   if (this.col_drag_active && this.col_draglayer) {
1619     var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
1620
1621     for (i=0; i<this.cols.length; i++) {
1622       if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
1623         cpos += this.cols[i];
1624       else
1625         break;
1626     }
1627
1628     // handle fixed columns on left
1629     if (i == 0 && this.list_min_pos > pos.x)
1630       cpos = this.list_min_pos - this.list_pos;
1631     // empty list needs some assignment
1632     else if (!this.list.rowcount && i == this.cols.length)
1633       cpos -= 2;
1634     $('#rcmcolumnindicator').css({ width: cpos+'px'});
1635     this.triggerEvent('column_dragmove', e?e:window.event);
1636   }
1637
1638   this.drag_start = false;
1639
1640   return false;
1641 },
1642
1643
1644 /**
1645  * Handler for mouse up events for dragging list columns
1646  */
1647 column_drag_mouse_up: function(e)
1648 {
1649   document.onmousemove = null;
1650
1651   if (this.col_draglayer) {
1652     (this.col_draglayer).remove();
1653     this.col_draglayer = null;
1654   }
1655
1656   if (this.col_drag_active)
1657     this.focus();
1658   this.col_drag_active = false;
1659
1660   rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
1661   rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
1662   // remove temp divs
1663   this.del_dragfix();
1664
1665   if (this.selected_column !== null && this.cols && this.cols.length) {
1666     var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
1667
1668     // find destination position
1669     for (i=0; i<this.cols.length; i++) {
1670       if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
1671         cpos += this.cols[i];
1672       else
1673         break;
1674     }
1675
1676     if (i != this.selected_column && i != this.selected_column+1) {
1677       this.column_replace(this.selected_column, i);
1678     }
1679   }
1680
76a98d 1681   this.triggerEvent('column_dragend', e);
b62c48 1682
A 1683   return rcube_event.cancel(e);
1684 },
1685
1686
1687 /**
2b55d4 1688  * Returns IDs of all rows in a thread (except root) for specified root
AM 1689  */
1690 row_children: function(uid)
1691 {
1692   if (!this.rows[uid] || !this.rows[uid].has_children)
1693     return [];
1694
1695   var res = [], depth = this.rows[uid].depth,
1696     row = this.rows[uid].obj.nextSibling;
1697
1698   while (row) {
1699     if (row.nodeType == 1) {
7eecf8 1700       if (r = this.rows[row.uid]) {
2b55d4 1701         if (!r.depth || r.depth <= depth)
AM 1702           break;
1703         res.push(r.uid);
1704       }
1705     }
1706     row = row.nextSibling;
1707   }
1708
1709   return res;
1710 },
1711
1712
1713 /**
b62c48 1714  * Creates a layer for drag&drop over iframes
A 1715  */
1716 add_dragfix: function()
1717 {
1718   $('iframe').each(function() {
1719     $('<div class="iframe-dragdrop-fix"></div>')
1720       .css({background: '#fff',
1721         width: this.offsetWidth+'px', height: this.offsetHeight+'px',
1722         position: 'absolute', opacity: '0.001', zIndex: 1000
1723       })
1724       .css($(this).offset())
1725       .appendTo(document.body);
677e1f 1726   });
b62c48 1727 },
A 1728
1729
1730 /**
1731  * Removes the layer for drag&drop over iframes
1732  */
1733 del_dragfix: function()
1734 {
acc900 1735   $('div.iframe-dragdrop-fix').remove();
b62c48 1736 },
A 1737
1738
1739 /**
1740  * Replaces two columns
1741  */
1742 column_replace: function(from, to)
1743 {
517dae 1744   // only supported for <table> lists
TB 1745   if (!this.thead || !this.thead.rows)
1746     return;
1747
1748   var len, cells = this.thead.rows[0].cells,
b62c48 1749     elem = cells[from],
A 1750     before = cells[to],
1751     td = document.createElement('td');
1752
1753   // replace header cells
1754   if (before)
1755     cells[0].parentNode.insertBefore(td, before);
1756   else
1757     cells[0].parentNode.appendChild(td);
1758   cells[0].parentNode.replaceChild(elem, td);
1759
1760   // replace list cells
517dae 1761   for (r=0, len=this.tbody.rows.length; r<len; r++) {
TB 1762     row = this.tbody.rows[r];
b62c48 1763
A 1764     elem = row.cells[from];
1765     before = row.cells[to];
1766     td = document.createElement('td');
1767
1768     if (before)
1769       row.insertBefore(td, before);
1770     else
1771       row.appendChild(td);
1772     row.replaceChild(elem, td);
1773   }
1774
1775   // update subject column position
1776   if (this.subject_col == from)
1777     this.subject_col = to > from ? to - 1 : to;
8e32dc 1778   else if (this.subject_col < from && to <= this.subject_col)
T 1779     this.subject_col++;
1780   else if (this.subject_col > from && to >= this.subject_col)
1781     this.subject_col--;
b62c48 1782
73ad4f 1783   if (this.fixed_header)
TB 1784     this.init_header();
1785
b62c48 1786   this.triggerEvent('column_replace');
6b47de 1787 }
T 1788
1789 };
1790
cc97ea 1791 rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
T 1792 rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
1793 rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;