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