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