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