alecpl
2008-08-29 910d07e3002a9077500e09abea968fc7f2eaeb91
commit | author | age
6b47de 1 /*
T 2  +-----------------------------------------------------------------------+
3  | RoundCube List Widget                                                 |
4  |                                                                       |
5  | This file is part of the RoundCube Webmail client                     |
0b7cd3 6  | Copyright (C) 2006-2008, RoundCube Dev, - Switzerland                 |
6b47de 7  | Licensed under the GNU GPL                                            |
T 8  |                                                                       |
9  +-----------------------------------------------------------------------+
10  | Authors: Thomas Bruederli <roundcube@gmail.com>                       |
11  |          Charles McNulty <charles@charlesmcnulty.com>                 |
12  +-----------------------------------------------------------------------+
13  | Requires: common.js                                                   |
14  +-----------------------------------------------------------------------+
15
16   $Id: list.js 344 2006-09-18 03:49:28Z thomasb $
17 */
18
19
20 /**
21  * RoundCube List Widget class
22  * @contructor
23  */
24 function rcube_list_widget(list, p)
25   {
26   // static contants
27   this.ENTER_KEY = 13;
28   this.DELETE_KEY = 46;
29   
30   this.list = list ? list : null;
31   this.frame = null;
32   this.rows = [];
33   this.selection = [];
34   
d24d20 35   this.subject_col = -1;
31c171 36   this.shiftkey = false;
6b47de 37   this.multiselect = false;
21168d 38   this.multi_selecting = false;
6b47de 39   this.draggable = false;
T 40   this.keyboard = false;
68b6a9 41   this.toggleselect = false;
6b47de 42   
T 43   this.dont_select = false;
44   this.drag_active = false;
45   this.last_selected = 0;
b2fb95 46   this.shift_start = 0;
6b47de 47   this.in_selection_before = false;
T 48   this.focused = false;
49   this.drag_mouse_start = null;
50   this.dblclick_time = 600;
51   this.row_init = function(){};
52   this.events = { click:[], dblclick:[], select:[], keypress:[], dragstart:[], dragend:[] };
53   
54   // overwrite default paramaters
55   if (p && typeof(p)=='object')
56     for (var n in p)
57       this[n] = p[n];
58   }
59
60
61 rcube_list_widget.prototype = {
62
63
64 /**
65  * get all message rows from HTML table and init each row
66  */
67 init: function()
68 {
69   if (this.list && this.list.tBodies[0])
70   {
71     this.rows = new Array();
72
73     var row;
74     for(var r=0; r<this.list.tBodies[0].childNodes.length; r++)
75     {
76       row = this.list.tBodies[0].childNodes[r];
77       while (row && (row.nodeType != 1 || row.style.display == 'none'))
78       {
79         row = row.nextSibling;
80         r++;
81       }
82
83       this.init_row(row);
84     }
85
86     this.frame = this.list.parentNode;
87
88     // set body events
26f5b0 89     if (this.keyboard) {
T 90       rcube_event.add_listener({element:document, event:'keyup', object:this, method:'key_press'});
9e7a1b 91       rcube_event.add_listener({element:document, event:'keydown', object:this, method:'key_down'});
26f5b0 92     }
6b47de 93   }
T 94 },
95
96
97 /**
98  *
99  */
100 init_row: function(row)
101 {
102   // make references in internal array and set event handlers
f11541 103   if (row && String(row.id).match(/rcmrow([a-z0-9\-_=]+)/i))
6b47de 104   {
T 105     var p = this;
106     var uid = RegExp.$1;
107     row.uid = uid;
108     this.rows[uid] = {uid:uid, id:row.id, obj:row, classname:row.className};
109
110     // set eventhandlers to table row
111     row.onmousedown = function(e){ return p.drag_row(e, this.uid); };
112     row.onmouseup = function(e){ return p.click_row(e, this.uid); };
113
114     if (document.all)
115       row.onselectstart = function() { return false; };
116
117     this.row_init(this.rows[uid]);
118   }
119 },
120
121
122 /**
123  *
124  */
f11541 125 clear: function(sel)
6b47de 126 {
T 127   var tbody = document.createElement('TBODY');
128   this.list.insertBefore(tbody, this.list.tBodies[0]);
129   this.list.removeChild(this.list.tBodies[1]);
f11541 130   this.rows = new Array();
T 131   
132   if (sel) this.clear_selection();
6b47de 133 },
T 134
135
136 /**
137  * 'remove' message row from list (just hide it)
138  */
b62656 139 remove_row: function(uid, sel_next)
6b47de 140 {
T 141   if (this.rows[uid].obj)
142     this.rows[uid].obj.style.display = 'none';
143
b62656 144   if (sel_next)
T 145     this.select_next();
146
6b47de 147   this.rows[uid] = null;
T 148 },
149
150
151 /**
152  *
153  */
154 insert_row: function(row, attop)
155 {
156   var tbody = this.list.tBodies[0];
157
158   if (attop && tbody.rows.length)
159     tbody.insertBefore(row, tbody.firstChild);
160   else
161     tbody.appendChild(row);
162
163   this.init_row(row);
164 },
165
166
167
168 /**
169  * Set focur to the list
170  */
171 focus: function(e)
172 {
173   this.focused = true;
174   for (var n=0; n<this.selection.length; n++)
175   {
176     id = this.selection[n];
e78864 177     if (this.rows[id] && this.rows[id].obj)
6b47de 178     {
T 179       this.set_classname(this.rows[id].obj, 'selected', true);
180       this.set_classname(this.rows[id].obj, 'unfocused', false);
181     }
182   }
183
184   if (e || (e = window.event))
185     rcube_event.cancel(e);
186 },
187
188
189 /**
190  * remove focus from the list
191  */
192 blur: function()
193 {
194   var id;
195   this.focused = false;
196   for (var n=0; n<this.selection.length; n++)
197   {
198     id = this.selection[n];
199     if (this.rows[id] && this.rows[id].obj)
200     {
201       this.set_classname(this.rows[id].obj, 'selected', false);
202       this.set_classname(this.rows[id].obj, 'unfocused', true);
203     }
204   }
205 },
206
207
208 /**
209  * onmousedown-handler of message list row
210  */
211 drag_row: function(e, id)
212 {
213   // don't do anything (another action processed before)
a0ce2f 214   var evtarget = rcube_event.get_target(e);
T 215   if (this.dont_select || (evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
6b47de 216     return false;
T 217
bf36a9 218   this.in_selection_before = this.in_selection(id) ? id : false;
T 219
6b47de 220   // selects currently unselected row
T 221   if (!this.in_selection_before)
222   {
223     var mod_key = rcube_event.get_modifier(e);
224     this.select_row(id, mod_key, false);
225   }
226
227   if (this.draggable && this.selection.length)
228   {
229     this.drag_start = true;
b2fb95 230     this.drag_mouse_start = rcube_event.get_mouse_pos(e);
6b47de 231     rcube_event.add_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
T 232     rcube_event.add_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
233   }
234
235   return false;
236 },
237
238
239 /**
240  * onmouseup-handler of message list row
241  */
242 click_row: function(e, id)
243 {
244   var now = new Date().getTime();
245   var mod_key = rcube_event.get_modifier(e);
a0ce2f 246   var evtarget = rcube_event.get_target(e);
0b7cd3 247   
a0ce2f 248   if ((evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
0b7cd3 249     return false;
T 250   
6b47de 251   // don't do anything (another action processed before)
T 252   if (this.dont_select)
253     {
254     this.dont_select = false;
255     return false;
256     }
257     
258   var dblclicked = now - this.rows[id].clicked < this.dblclick_time;
259
260   // unselects currently selected row
261   if (!this.drag_active && this.in_selection_before == id && !dblclicked)
262     this.select_row(id, mod_key, false);
263
264   this.drag_start = false;
265   this.in_selection_before = false;
266
267   // row was double clicked
268   if (this.rows && dblclicked && this.in_selection(id))
269     this.trigger_event('dblclick');
270   else
271     this.trigger_event('click');
272
273   if (!this.drag_active)
274     rcube_event.cancel(e);
275
276   this.rows[id].clicked = now;
277   return false;
278 },
279
280
281 /**
282  * get next and previous rows that are not hidden
283  */
284 get_next_row: function()
285 {
286   if (!this.rows)
287     return false;
288
289   var last_selected_row = this.rows[this.last_selected];
a7d5c6 290   var new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
6b47de 291   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
T 292     new_row = new_row.nextSibling;
293
294   return new_row;
295 },
296
297 get_prev_row: function()
298 {
299   if (!this.rows)
300     return false;
301
302   var last_selected_row = this.rows[this.last_selected];
a7d5c6 303   var new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
6b47de 304   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
T 305     new_row = new_row.previousSibling;
306
307   return new_row;
308 },
309
310
311 // selects or unselects the proper row depending on the modifier key pressed
312 select_row: function(id, mod_key, with_mouse)
313 {
314   var select_before = this.selection.join(',');
315   if (!this.multiselect)
316     mod_key = 0;
b2fb95 317     
T 318   if (!this.shift_start)
319     this.shift_start = id
6b47de 320
T 321   if (!mod_key)
322   {
323     this.shift_start = id;
324     this.highlight_row(id, false);
21168d 325     this.multi_selecting = false;
6b47de 326   }
T 327   else
328   {
329     switch (mod_key)
330     {
331       case SHIFT_KEY:
b2fb95 332         this.shift_select(id, false);
6b47de 333         break;
T 334
335       case CONTROL_KEY:
336         if (!with_mouse)
b2fb95 337           this.highlight_row(id, true);
6b47de 338         break; 
T 339
340       case CONTROL_SHIFT_KEY:
341         this.shift_select(id, true);
342         break;
343
344       default:
b2fb95 345         this.highlight_row(id, false);
6b47de 346         break;
T 347     }
21168d 348     this.multi_selecting = true;
6b47de 349   }
T 350
351   // trigger event if selection changed
352   if (this.selection.join(',') != select_before)
353     this.trigger_event('select');
354
355   if (this.last_selected != 0 && this.rows[this.last_selected])
356     this.set_classname(this.rows[this.last_selected].obj, 'focused', false);
a9dda5 357
T 358   // unselect if toggleselect is active and the same row was clicked again
359   if (this.toggleselect && this.last_selected == id)
360   {
361     this.clear_selection();
362     id = null;
363   }
364   else
365     this.set_classname(this.rows[id].obj, 'focused', true);
366
b2fb95 367   if (!this.selection.length)
T 368     this.shift_start = null;
6b47de 369
T 370   this.last_selected = id;
371 },
372
373
374 /**
375  * Alias method for select_row
376  */
377 select: function(id)
378 {
379   this.select_row(id, false);
380   this.scrollto(id);
381 },
382
383
384 /**
385  * Select row next to the last selected one.
386  * Either below or above.
387  */
388 select_next: function()
389 {
390   var next_row = this.get_next_row();
391   var prev_row = this.get_prev_row();
392   var new_row = (next_row) ? next_row : prev_row;
393   if (new_row)
394     this.select_row(new_row.uid, false, false);  
395 },
396
397
398 /**
399  * Perform selection when shift key is pressed
400  */
401 shift_select: function(id, control)
402 {
b00bd0 403   if (!this.rows[this.shift_start] || !this.selection.length)
f2892d 404     this.shift_start = id;
A 405
6b47de 406   var from_rowIndex = this.rows[this.shift_start].obj.rowIndex;
T 407   var to_rowIndex = this.rows[id].obj.rowIndex;
408
409   var i = ((from_rowIndex < to_rowIndex)? from_rowIndex : to_rowIndex);
410   var j = ((from_rowIndex > to_rowIndex)? from_rowIndex : to_rowIndex);
411
412   // iterate through the entire message list
413   for (var n in this.rows)
414   {
415     if ((this.rows[n].obj.rowIndex >= i) && (this.rows[n].obj.rowIndex <= j))
416     {
417       if (!this.in_selection(n))
418         this.highlight_row(n, true);
419     }
420     else
421     {
422       if  (this.in_selection(n) && !control)
423         this.highlight_row(n, true);
424     }
425   }
426 },
427
428
429 /**
430  * Check if given id is part of the current selection
431  */
432 in_selection: function(id)
433 {
434   for(var n in this.selection)
435     if (this.selection[n]==id)
436       return true;
437
438   return false;    
439 },
440
441
442 /**
443  * Select each row in list
444  */
445 select_all: function(filter)
446 {
447   if (!this.rows || !this.rows.length)
448     return false;
449
1094cd 450   // reset but remember selection first
T 451   var select_before = this.selection.join(',');
6b47de 452   this.clear_selection();
T 453
454   for (var n in this.rows)
455   {
456     if (!filter || this.rows[n][filter]==true)
457     {
458       this.last_selected = n;
459       this.highlight_row(n, true);
460     }
461   }
462
1094cd 463   // trigger event if selection changed
T 464   if (this.selection.join(',') != select_before)
465     this.trigger_event('select');
466
d7c226 467   this.focus();
A 468
1094cd 469   return true;
6b47de 470 },
T 471
472
473 /**
474  * Unselect all selected rows
475  */
476 clear_selection: function()
477 {
1094cd 478   var num_select = this.selection.length;
T 479   for (var n=0; n<this.selection.length; n++)
6b47de 480     if (this.rows[this.selection[n]])
T 481     {
482       this.set_classname(this.rows[this.selection[n]].obj, 'selected', false);
483       this.set_classname(this.rows[this.selection[n]].obj, 'unfocused', false);
484     }
485
1094cd 486   this.selection = new Array();
T 487   
488   if (num_select)
489     this.trigger_event('select');
6b47de 490 },
T 491
492
493 /**
494  * Getter for the selection array
495  */
496 get_selection: function()
497 {
498   return this.selection;
499 },
500
501
502 /**
503  * Return the ID if only one row is selected
504  */
505 get_single_selection: function()
506 {
507   if (this.selection.length == 1)
508     return this.selection[0];
509   else
510     return null;
511 },
512
513
514 /**
515  * Highlight/unhighlight a row
516  */
517 highlight_row: function(id, multiple)
518 {
519   if (this.rows[id] && !multiple)
520   {
df015d 521     if (this.selection.length > 1 || !this.in_selection(id))
T 522     {
523       this.clear_selection();
524       this.selection[0] = id;
525       this.set_classname(this.rows[id].obj, 'selected', true);
526     }
6b47de 527   }
T 528   else if (this.rows[id])
529   {
530     if (!this.in_selection(id))  // select row
531     {
532       this.selection[this.selection.length] = id;
533       this.set_classname(this.rows[id].obj, 'selected', true);
534     }
535     else  // unselect row
536     {
537       var p = find_in_array(id, this.selection);
538       var a_pre = this.selection.slice(0, p);
539       var a_post = this.selection.slice(p+1, this.selection.length);
540       this.selection = a_pre.concat(a_post);
541       this.set_classname(this.rows[id].obj, 'selected', false);
542       this.set_classname(this.rows[id].obj, 'unfocused', false);
543     }
544   }
545 },
546
547
548 /**
549  * Handler for keyboard events
550  */
551 key_press: function(e)
552 {
26f5b0 553   if (this.focused != true)
6b47de 554     return true;
T 555
26f5b0 556   var keyCode = rcube_event.get_keycode(e);
6b47de 557   var mod_key = rcube_event.get_modifier(e);
T 558   switch (keyCode)
559   {
560     case 40:
561     case 38: 
26f5b0 562     case 63233: // "down", in safari keypress
T 563     case 63232: // "up", in safari keypress
564       // Stop propagation so that the browser doesn't scroll
565       rcube_event.cancel(e);
6b47de 566       return this.use_arrow_key(keyCode, mod_key);
T 567     default:
110748 568       this.shiftkey = e.shiftKey;
6b47de 569       this.key_pressed = keyCode;
T 570       this.trigger_event('keypress');
571   }
572   
573   return true;
574 },
575
9e7a1b 576 /**
T 577  * Handler for keydown events
578  */
579 key_down: function(e)
580 {
581   switch (rcube_event.get_keycode(e))
582   {
583     case 40:
584     case 38: 
585     case 63233:
586     case 63232:
587       if (!rcube_event.get_modifier(e) && this.focused)
588         return rcube_event.cancel(e);
589         
590     default:
591   }
592   
593   return true;
594 },
595
6b47de 596
T 597 /**
598  * Special handling method for arrow keys
599  */
600 use_arrow_key: function(keyCode, mod_key)
601 {
602   var new_row;
e9b57b 603   // Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
A 604   // using the keypress event (but not the keydown or keyup event).
605   if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
6b47de 606     new_row = this.get_next_row();
e9b57b 607   else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
6b47de 608     new_row = this.get_prev_row();
T 609
610   if (new_row)
611   {
612     this.select_row(new_row.uid, mod_key, true);
613     this.scrollto(new_row.uid);
614   }
615
616   return false;
617 },
618
619
620 /**
621  * Try to scroll the list to make the specified row visible
622  */
623 scrollto: function(id)
624 {
625   var row = this.rows[id].obj;
626   if (row && this.frame)
627   {
628     var scroll_to = Number(row.offsetTop);
629
630     if (scroll_to < Number(this.frame.scrollTop))
631       this.frame.scrollTop = scroll_to;
632     else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
633       this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
634   }
635 },
636
637
638 /**
639  * Handler for mouse move events
640  */
641 drag_mouse_move: function(e)
642 {
643   if (this.drag_start)
644   {
645     // check mouse movement, of less than 3 pixels, don't start dragging
646     var m = rcube_event.get_mouse_pos(e);
647     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))
648       return false;
649   
650     if (!this.draglayer)
651       this.draglayer = new rcube_layer('rcmdraglayer', {x:0, y:0, width:300, vis:0, zindex:2000});
652   
653     // get subjects of selectedd messages
654     var names = '';
d24d20 655     var c, i, node, subject, obj;
6b47de 656     for(var n=0; n<this.selection.length; n++)
T 657     {
658       if (n>12)  // only show 12 lines
659       {
660         names += '...';
661         break;
662       }
663
664       if (this.rows[this.selection[n]].obj)
665       {
666         obj = this.rows[this.selection[n]].obj;
667         subject = '';
668
d24d20 669         for(c=0, i=0; i<obj.childNodes.length; i++)
T 670         {
671           if (obj.childNodes[i].nodeName == 'TD')
6b47de 672           {
d24d20 673             if (((node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) &&
T 674               (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)))
675             {
676               subject = node.nodeType==3 ? node.data : node.innerHTML;
677               names += (subject.length > 50 ? subject.substring(0, 50)+'...' : subject) + '<br />';
678               break;
679             }
680             c++;
6b47de 681           }
d24d20 682         }
6b47de 683       }
T 684     }
685
686     this.draglayer.write(names);
687     this.draglayer.show(1);
688
689     this.drag_active = true;
690     this.trigger_event('dragstart');
691   }
692
693   if (this.drag_active && this.draglayer)
694   {
695     var pos = rcube_event.get_mouse_pos(e);
696     this.draglayer.move(pos.x+20, pos.y-5);
697   }
698
699   this.drag_start = false;
700
701   return false;
702 },
703
704
705 /**
706  * Handler for mouse up events
707  */
708 drag_mouse_up: function(e)
709 {
710   document.onmousemove = null;
711
712   if (this.draglayer && this.draglayer.visible)
713     this.draglayer.show(0);
714
715   this.drag_active = false;
716   this.trigger_event('dragend');
717
718   rcube_event.remove_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
719   rcube_event.remove_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
720
287227 721   this.focus();
A 722   
6b47de 723   return rcube_event.cancel(e);
T 724 },
725
726
727
728 /**
729  * set/unset a specific class name
730  */
731 set_classname: function(obj, classname, set)
732 {
733   var reg = new RegExp('\s*'+classname, 'i');
734   if (!set && obj.className.match(reg))
735     obj.className = obj.className.replace(reg, '');
736   else if (set && !obj.className.match(reg))
737     obj.className += ' '+classname;
738 },
739
740
741 /**
742  * Setter for object event handlers
743  *
744  * @param {String}   Event name
745  * @param {Function} Handler function
746  * @return Listener ID (used to remove this handler later on)
747  */
748 addEventListener: function(evt, handler)
749 {
750   if (this.events[evt]) {
751     var handle = this.events[evt].length;
752     this.events[evt][handle] = handler;
753     return handle;
754   }
755   else
756     return false;
757 },
758
759
760 /**
761  * Removes a specific event listener
762  *
763  * @param {String} Event name
764  * @param {Int}    Listener ID to remove
765  */
766 removeEventListener: function(evt, handle)
767 {
768   if (this.events[evt] && this.events[evt][handle])
769     this.events[evt][handle] = null;
770 },
771
772
773 /**
774  * This will execute all registered event handlers
775  * @private
776  */
777 trigger_event: function(evt)
778 {
779   if (this.events[evt] && this.events[evt].length) {
780     for (var i=0; i<this.events[evt].length; i++)
781       if (typeof(this.events[evt][i]) == 'function')
782         this.events[evt][i](this);
783   }
784 }
785
786
787 };
788