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