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