]> git.donarmstrong.com Git - roundcube.git/blob - program/js/list.js
Imported Upstream version 0.1
[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-2008, 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.subject_col = -1;
36   this.shiftkey = false;
37   this.multiselect = false;
38   this.draggable = false;
39   this.keyboard = false;
40   this.toggleselect = false;
41   
42   this.dont_select = false;
43   this.drag_active = false;
44   this.last_selected = 0;
45   this.shift_start = 0;
46   this.in_selection_before = false;
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
100   if (row && String(row.id).match(/rcmrow([a-z0-9\-_=]+)/i))
101   {
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  */
122 clear: function(sel)
123 {
124   var tbody = document.createElement('TBODY');
125   this.list.insertBefore(tbody, this.list.tBodies[0]);
126   this.list.removeChild(this.list.tBodies[1]);
127   this.rows = new Array();
128   
129   if (sel) this.clear_selection();
130 },
131
132
133 /**
134  * 'remove' message row from list (just hide it)
135  */
136 remove_row: function(uid, sel_next)
137 {
138   if (this.rows[uid].obj)
139     this.rows[uid].obj.style.display = 'none';
140
141   if (sel_next)
142     this.select_next();
143
144   this.rows[uid] = null;
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   // don't do anything (another action processed before)
211   var evtarget = rcube_event.get_target(e);
212   if (this.dont_select || (evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
213     return false;
214
215   this.in_selection_before = this.in_selection(id) ? id : false;
216
217   // selects currently unselected row
218   if (!this.in_selection_before)
219   {
220     var mod_key = rcube_event.get_modifier(e);
221     this.select_row(id, mod_key, false);
222   }
223
224   if (this.draggable && this.selection.length)
225   {
226     this.drag_start = true;
227     this.drag_mouse_start = rcube_event.get_mouse_pos(e);
228     rcube_event.add_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
229     rcube_event.add_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
230   }
231
232   return false;
233 },
234
235
236 /**
237  * onmouseup-handler of message list row
238  */
239 click_row: function(e, id)
240 {
241   var now = new Date().getTime();
242   var mod_key = rcube_event.get_modifier(e);
243   var evtarget = rcube_event.get_target(e);
244   
245   if ((evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG')))
246     return false;
247   
248   // don't do anything (another action processed before)
249   if (this.dont_select)
250     {
251     this.dont_select = false;
252     return false;
253     }
254     
255   var dblclicked = now - this.rows[id].clicked < this.dblclick_time;
256
257   // unselects currently selected row
258   if (!this.drag_active && this.in_selection_before == id && !dblclicked)
259     this.select_row(id, mod_key, false);
260
261   this.drag_start = false;
262   this.in_selection_before = false;
263
264   // row was double clicked
265   if (this.rows && dblclicked && this.in_selection(id))
266     this.trigger_event('dblclick');
267   else
268     this.trigger_event('click');
269
270   if (!this.drag_active)
271     rcube_event.cancel(e);
272
273   this.rows[id].clicked = now;
274   return false;
275 },
276
277
278 /**
279  * get next and previous rows that are not hidden
280  */
281 get_next_row: function()
282 {
283   if (!this.rows)
284     return false;
285
286   var last_selected_row = this.rows[this.last_selected];
287   var new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
288   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
289     new_row = new_row.nextSibling;
290
291   return new_row;
292 },
293
294 get_prev_row: function()
295 {
296   if (!this.rows)
297     return false;
298
299   var last_selected_row = this.rows[this.last_selected];
300   var new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
301   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
302     new_row = new_row.previousSibling;
303
304   return new_row;
305 },
306
307
308 // selects or unselects the proper row depending on the modifier key pressed
309 select_row: function(id, mod_key, with_mouse)
310 {
311   var select_before = this.selection.join(',');
312   if (!this.multiselect)
313     mod_key = 0;
314     
315   if (!this.shift_start)
316     this.shift_start = id
317
318   if (!mod_key)
319   {
320     this.shift_start = id;
321     this.highlight_row(id, false);
322   }
323   else
324   {
325     switch (mod_key)
326     {
327       case SHIFT_KEY:
328         this.shift_select(id, false);
329         break;
330
331       case CONTROL_KEY:
332         if (!with_mouse)
333           this.highlight_row(id, true);
334         break; 
335
336       case CONTROL_SHIFT_KEY:
337         this.shift_select(id, true);
338         break;
339
340       default:
341         this.highlight_row(id, false);
342         break;
343     }
344   }
345
346   // trigger event if selection changed
347   if (this.selection.join(',') != select_before)
348     this.trigger_event('select');
349
350   if (this.last_selected != 0 && this.rows[this.last_selected])
351     this.set_classname(this.rows[this.last_selected].obj, 'focused', false);
352
353   // unselect if toggleselect is active and the same row was clicked again
354   if (this.toggleselect && this.last_selected == id)
355   {
356     this.clear_selection();
357     id = null;
358   }
359   else
360     this.set_classname(this.rows[id].obj, 'focused', true);
361
362   if (!this.selection.length)
363     this.shift_start = null;
364
365   this.last_selected = id;
366 },
367
368
369 /**
370  * Alias method for select_row
371  */
372 select: function(id)
373 {
374   this.select_row(id, false);
375   this.scrollto(id);
376 },
377
378
379 /**
380  * Select row next to the last selected one.
381  * Either below or above.
382  */
383 select_next: function()
384 {
385   var next_row = this.get_next_row();
386   var prev_row = this.get_prev_row();
387   var new_row = (next_row) ? next_row : prev_row;
388   if (new_row)
389     this.select_row(new_row.uid, false, false);  
390 },
391
392
393 /**
394  * Perform selection when shift key is pressed
395  */
396 shift_select: function(id, control)
397 {
398   var from_rowIndex = this.rows[this.shift_start].obj.rowIndex;
399   var to_rowIndex = this.rows[id].obj.rowIndex;
400
401   var i = ((from_rowIndex < to_rowIndex)? from_rowIndex : to_rowIndex);
402   var j = ((from_rowIndex > to_rowIndex)? from_rowIndex : to_rowIndex);
403
404   // iterate through the entire message list
405   for (var n in this.rows)
406   {
407     if ((this.rows[n].obj.rowIndex >= i) && (this.rows[n].obj.rowIndex <= j))
408     {
409       if (!this.in_selection(n))
410         this.highlight_row(n, true);
411     }
412     else
413     {
414       if  (this.in_selection(n) && !control)
415         this.highlight_row(n, true);
416     }
417   }
418 },
419
420
421 /**
422  * Check if given id is part of the current selection
423  */
424 in_selection: function(id)
425 {
426   for(var n in this.selection)
427     if (this.selection[n]==id)
428       return true;
429
430   return false;    
431 },
432
433
434 /**
435  * Select each row in list
436  */
437 select_all: function(filter)
438 {
439   if (!this.rows || !this.rows.length)
440     return false;
441
442   // reset but remember selection first
443   var select_before = this.selection.join(',');
444   this.clear_selection();
445
446   for (var n in this.rows)
447   {
448     if (!filter || this.rows[n][filter]==true)
449     {
450       this.last_selected = n;
451       this.highlight_row(n, true);
452     }
453   }
454
455   // trigger event if selection changed
456   if (this.selection.join(',') != select_before)
457     this.trigger_event('select');
458
459   return true;
460 },
461
462
463 /**
464  * Unselect all selected rows
465  */
466 clear_selection: function()
467 {
468   var num_select = this.selection.length;
469   for (var n=0; n<this.selection.length; n++)
470     if (this.rows[this.selection[n]])
471     {
472       this.set_classname(this.rows[this.selection[n]].obj, 'selected', false);
473       this.set_classname(this.rows[this.selection[n]].obj, 'unfocused', false);
474     }
475
476   this.selection = new Array();
477   
478   if (num_select)
479     this.trigger_event('select');
480 },
481
482
483 /**
484  * Getter for the selection array
485  */
486 get_selection: function()
487 {
488   return this.selection;
489 },
490
491
492 /**
493  * Return the ID if only one row is selected
494  */
495 get_single_selection: function()
496 {
497   if (this.selection.length == 1)
498     return this.selection[0];
499   else
500     return null;
501 },
502
503
504 /**
505  * Highlight/unhighlight a row
506  */
507 highlight_row: function(id, multiple)
508 {
509   if (this.rows[id] && !multiple)
510   {
511     if (!this.in_selection(id))
512     {
513       this.clear_selection();
514       this.selection[0] = id;
515       this.set_classname(this.rows[id].obj, 'selected', true);
516     }
517   }
518   else if (this.rows[id])
519   {
520     if (!this.in_selection(id))  // select row
521     {
522       this.selection[this.selection.length] = id;
523       this.set_classname(this.rows[id].obj, 'selected', true);
524     }
525     else  // unselect row
526     {
527       var p = find_in_array(id, this.selection);
528       var a_pre = this.selection.slice(0, p);
529       var a_post = this.selection.slice(p+1, this.selection.length);
530       this.selection = a_pre.concat(a_post);
531       this.set_classname(this.rows[id].obj, 'selected', false);
532       this.set_classname(this.rows[id].obj, 'unfocused', false);
533     }
534   }
535 },
536
537
538 /**
539  * Handler for keyboard events
540  */
541 key_press: function(e)
542 {
543   if (this.focused != true) 
544     return true;
545
546   var keyCode = document.layers ? e.which : document.all ? event.keyCode : document.getElementById ? e.keyCode : 0;
547   var mod_key = rcube_event.get_modifier(e);
548   switch (keyCode)
549   {
550     case 40:
551     case 38: 
552       return this.use_arrow_key(keyCode, mod_key);
553       break;
554
555     default:
556       this.shiftkey = e.shiftKey;
557       this.key_pressed = keyCode;
558       this.trigger_event('keypress');
559   }
560   
561   return true;
562 },
563
564
565 /**
566  * Special handling method for arrow keys
567  */
568 use_arrow_key: function(keyCode, mod_key)
569 {
570   var new_row;
571   if (keyCode == 40) // down arrow key pressed
572     new_row = this.get_next_row();
573   else if (keyCode == 38) // up arrow key pressed
574     new_row = this.get_prev_row();
575
576   if (new_row)
577   {
578     this.select_row(new_row.uid, mod_key, true);
579     this.scrollto(new_row.uid);
580   }
581
582   return false;
583 },
584
585
586 /**
587  * Try to scroll the list to make the specified row visible
588  */
589 scrollto: function(id)
590 {
591   var row = this.rows[id].obj;
592   if (row && this.frame)
593   {
594     var scroll_to = Number(row.offsetTop);
595
596     if (scroll_to < Number(this.frame.scrollTop))
597       this.frame.scrollTop = scroll_to;
598     else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
599       this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
600   }
601 },
602
603
604 /**
605  * Handler for mouse move events
606  */
607 drag_mouse_move: function(e)
608 {
609   if (this.drag_start)
610   {
611     // check mouse movement, of less than 3 pixels, don't start dragging
612     var m = rcube_event.get_mouse_pos(e);
613     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))
614       return false;
615   
616     if (!this.draglayer)
617       this.draglayer = new rcube_layer('rcmdraglayer', {x:0, y:0, width:300, vis:0, zindex:2000});
618   
619     // get subjects of selectedd messages
620     var names = '';
621     var c, i, node, subject, obj;
622     for(var n=0; n<this.selection.length; n++)
623     {
624       if (n>12)  // only show 12 lines
625       {
626         names += '...';
627         break;
628       }
629
630       if (this.rows[this.selection[n]].obj)
631       {
632         obj = this.rows[this.selection[n]].obj;
633         subject = '';
634
635         for(c=0, i=0; i<obj.childNodes.length; i++)
636         {
637           if (obj.childNodes[i].nodeName == 'TD')
638           {
639             if (((node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) &&
640               (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)))
641             {
642               subject = node.nodeType==3 ? node.data : node.innerHTML;
643               names += (subject.length > 50 ? subject.substring(0, 50)+'...' : subject) + '<br />';
644               break;
645             }
646             c++;
647           }
648         }
649       }
650     }
651
652     this.draglayer.write(names);
653     this.draglayer.show(1);
654
655     this.drag_active = true;
656     this.trigger_event('dragstart');
657   }
658
659   if (this.drag_active && this.draglayer)
660   {
661     var pos = rcube_event.get_mouse_pos(e);
662     this.draglayer.move(pos.x+20, pos.y-5);
663   }
664
665   this.drag_start = false;
666
667   return false;
668 },
669
670
671 /**
672  * Handler for mouse up events
673  */
674 drag_mouse_up: function(e)
675 {
676   document.onmousemove = null;
677
678   if (this.draglayer && this.draglayer.visible)
679     this.draglayer.show(0);
680
681   this.drag_active = false;
682   this.trigger_event('dragend');
683
684   rcube_event.remove_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
685   rcube_event.remove_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
686
687   return rcube_event.cancel(e);
688 },
689
690
691
692 /**
693  * set/unset a specific class name
694  */
695 set_classname: function(obj, classname, set)
696 {
697   var reg = new RegExp('\s*'+classname, 'i');
698   if (!set && obj.className.match(reg))
699     obj.className = obj.className.replace(reg, '');
700   else if (set && !obj.className.match(reg))
701     obj.className += ' '+classname;
702 },
703
704
705 /**
706  * Setter for object event handlers
707  *
708  * @param {String}   Event name
709  * @param {Function} Handler function
710  * @return Listener ID (used to remove this handler later on)
711  */
712 addEventListener: function(evt, handler)
713 {
714   if (this.events[evt]) {
715     var handle = this.events[evt].length;
716     this.events[evt][handle] = handler;
717     return handle;
718   }
719   else
720     return false;
721 },
722
723
724 /**
725  * Removes a specific event listener
726  *
727  * @param {String} Event name
728  * @param {Int}    Listener ID to remove
729  */
730 removeEventListener: function(evt, handle)
731 {
732   if (this.events[evt] && this.events[evt][handle])
733     this.events[evt][handle] = null;
734 },
735
736
737 /**
738  * This will execute all registered event handlers
739  * @private
740  */
741 trigger_event: function(evt)
742 {
743   if (this.events[evt] && this.events[evt].length) {
744     for (var i=0; i<this.events[evt].length; i++)
745       if (typeof(this.events[evt][i]) == 'function')
746         this.events[evt][i](this);
747   }
748 }
749
750
751 };
752