]> git.donarmstrong.com Git - roundcube.git/blob - program/js/list.js
Imported Upstream version 0.1~rc2
[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   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   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;
226     this.drag_mouse_start = rcube_event.get_mouse_pos(e);
227     rcube_event.add_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
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];
282   var new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
283   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
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];
295   var new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
296   while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
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;
309     
310   if (!this.shift_start)
311     this.shift_start = id
312
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:
323         this.shift_select(id, false);
324         break;
325
326       case CONTROL_KEY:
327         if (!with_mouse)
328           this.highlight_row(id, true);
329         break; 
330
331       case CONTROL_SHIFT_KEY:
332         this.shift_select(id, true);
333         break;
334
335       default:
336         this.highlight_row(id, false);
337         break;
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);
347
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
357   if (!this.selection.length)
358     this.shift_start = null;
359
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
437   // reset but remember selection first
438   var select_before = this.selection.join(',');
439   this.clear_selection();
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
450   // trigger event if selection changed
451   if (this.selection.join(',') != select_before)
452     this.trigger_event('select');
453
454   return true;
455 },
456
457
458 /**
459  * Unselect all selected rows
460  */
461 clear_selection: function()
462 {
463   var num_select = this.selection.length;
464   for (var n=0; n<this.selection.length; n++)
465     if (this.rows[this.selection[n]])
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
471   this.selection = new Array();
472   
473   if (num_select)
474     this.trigger_event('select');
475 },
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   {
506     this.clear_selection();
507     this.selection[0] = id;
508     this.set_classname(this.rows[id].obj, 'selected', true)
509   }
510   else if (this.rows[id])
511   {
512     if (!this.in_selection(id))  // select row
513     {
514       this.selection[this.selection.length] = id;
515       this.set_classname(this.rows[id].obj, 'selected', true);
516     }
517     else  // unselect row
518     {
519       var p = find_in_array(id, this.selection);
520       var a_pre = this.selection.slice(0, p);
521       var a_post = this.selection.slice(p+1, this.selection.length);
522       this.selection = a_pre.concat(a_post);
523       this.set_classname(this.rows[id].obj, 'selected', false);
524       this.set_classname(this.rows[id].obj, 'unfocused', false);
525     }
526   }
527 },
528
529
530 /**
531  * Handler for keyboard events
532  */
533 key_press: function(e)
534 {
535   if (this.focused != true) 
536     return true;
537
538   this.shiftkey = e.shiftKey;
539
540   var keyCode = document.layers ? e.which : document.all ? event.keyCode : document.getElementById ? e.keyCode : 0;
541   var mod_key = rcube_event.get_modifier(e);
542   switch (keyCode)
543   {
544     case 40:
545     case 38: 
546       return this.use_arrow_key(keyCode, mod_key);
547       break;
548
549     default:
550       this.key_pressed = keyCode;
551       this.trigger_event('keypress');
552   }
553   
554   return true;
555 },
556
557
558 /**
559  * Special handling method for arrow keys
560  */
561 use_arrow_key: function(keyCode, mod_key)
562 {
563   var new_row;
564   if (keyCode == 40) // down arrow key pressed
565     new_row = this.get_next_row();
566   else if (keyCode == 38) // up arrow key pressed
567     new_row = this.get_prev_row();
568
569   if (new_row)
570   {
571     this.select_row(new_row.uid, mod_key, true);
572     this.scrollto(new_row.uid);
573   }
574
575   return false;
576 },
577
578
579 /**
580  * Try to scroll the list to make the specified row visible
581  */
582 scrollto: function(id)
583 {
584   var row = this.rows[id].obj;
585   if (row && this.frame)
586   {
587     var scroll_to = Number(row.offsetTop);
588
589     if (scroll_to < Number(this.frame.scrollTop))
590       this.frame.scrollTop = scroll_to;
591     else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
592       this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
593   }
594 },
595
596
597 /**
598  * Handler for mouse move events
599  */
600 drag_mouse_move: function(e)
601 {
602   if (this.drag_start)
603   {
604     // check mouse movement, of less than 3 pixels, don't start dragging
605     var m = rcube_event.get_mouse_pos(e);
606     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))
607       return false;
608   
609     if (!this.draglayer)
610       this.draglayer = new rcube_layer('rcmdraglayer', {x:0, y:0, width:300, vis:0, zindex:2000});
611   
612     // get subjects of selectedd messages
613     var names = '';
614     var c, node, subject, obj;
615     for(var n=0; n<this.selection.length; n++)
616     {
617       if (n>12)  // only show 12 lines
618       {
619         names += '...';
620         break;
621       }
622
623       if (this.rows[this.selection[n]].obj)
624       {
625         obj = this.rows[this.selection[n]].obj;
626         subject = '';
627
628         for(c=0; c<obj.childNodes.length; c++)
629           if (obj.childNodes[c].nodeName=='TD' && (node = obj.childNodes[c].firstChild) && (node.nodeType==3 || node.nodeName=='A'))
630           {
631             subject = node.nodeType==3 ? node.data : node.innerHTML;
632             names += (subject.length > 50 ? subject.substring(0, 50)+'...' : subject) + '<br />';
633             break;
634           }
635       }
636     }
637
638     this.draglayer.write(names);
639     this.draglayer.show(1);
640
641     this.drag_active = true;
642     this.trigger_event('dragstart');
643   }
644
645   if (this.drag_active && this.draglayer)
646   {
647     var pos = rcube_event.get_mouse_pos(e);
648     this.draglayer.move(pos.x+20, pos.y-5);
649   }
650
651   this.drag_start = false;
652
653   return false;
654 },
655
656
657 /**
658  * Handler for mouse up events
659  */
660 drag_mouse_up: function(e)
661 {
662   document.onmousemove = null;
663
664   if (this.draglayer && this.draglayer.visible)
665     this.draglayer.show(0);
666
667   this.drag_active = false;
668   this.trigger_event('dragend');
669
670   rcube_event.remove_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'});
671   rcube_event.remove_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'});
672
673   return rcube_event.cancel(e);
674 },
675
676
677
678 /**
679  * set/unset a specific class name
680  */
681 set_classname: function(obj, classname, set)
682 {
683   var reg = new RegExp('\s*'+classname, 'i');
684   if (!set && obj.className.match(reg))
685     obj.className = obj.className.replace(reg, '');
686   else if (set && !obj.className.match(reg))
687     obj.className += ' '+classname;
688 },
689
690
691 /**
692  * Setter for object event handlers
693  *
694  * @param {String}   Event name
695  * @param {Function} Handler function
696  * @return Listener ID (used to remove this handler later on)
697  */
698 addEventListener: function(evt, handler)
699 {
700   if (this.events[evt]) {
701     var handle = this.events[evt].length;
702     this.events[evt][handle] = handler;
703     return handle;
704   }
705   else
706     return false;
707 },
708
709
710 /**
711  * Removes a specific event listener
712  *
713  * @param {String} Event name
714  * @param {Int}    Listener ID to remove
715  */
716 removeEventListener: function(evt, handle)
717 {
718   if (this.events[evt] && this.events[evt][handle])
719     this.events[evt][handle] = null;
720 },
721
722
723 /**
724  * This will execute all registered event handlers
725  * @private
726  */
727 trigger_event: function(evt)
728 {
729   if (this.events[evt] && this.events[evt].length) {
730     for (var i=0; i<this.events[evt].length; i++)
731       if (typeof(this.events[evt][i]) == 'function')
732         this.events[evt][i](this);
733   }
734 }
735
736
737 };
738