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