2 +-----------------------------------------------------------------------+
3 | Roundcube List Widget |
5 | This file is part of the Roundcube Webmail client |
6 | Copyright (C) 2006-2009, The Roundcube Dev Team |
7 | Licensed under the GNU GPL |
9 +-----------------------------------------------------------------------+
10 | Authors: Thomas Bruederli <roundcube@gmail.com> |
11 | Charles McNulty <charles@charlesmcnulty.com> |
12 +-----------------------------------------------------------------------+
13 | Requires: common.js |
14 +-----------------------------------------------------------------------+
16 $Id: list.js 5271 2011-09-22 20:51:42Z thomasb $
21 * Roundcube List Widget class
24 function rcube_list_widget(list, p)
29 this.BACKSPACE_KEY = 8;
31 this.list = list ? list : null;
38 this.subject_col = -1;
40 this.multiselect = false;
41 this.multiexpand = false;
42 this.multi_selecting = false;
43 this.draggable = false;
44 this.column_movable = false;
45 this.keyboard = false;
46 this.toggleselect = false;
48 this.dont_select = false;
49 this.drag_active = false;
50 this.col_drag_active = false;
51 this.column_fixed = null;
52 this.last_selected = 0;
54 this.in_selection_before = false;
56 this.drag_mouse_start = null;
57 this.dblclick_time = 600;
58 this.row_init = function(){};
60 // overwrite default paramaters
61 if (p && typeof p === 'object')
67 rcube_list_widget.prototype = {
71 * get all message rows from HTML table and init each row
75 if (this.list && this.list.tBodies[0]) {
79 var r, len, rows = this.list.tBodies[0].rows;
81 for (r=0, len=rows.length; r<len; r++) {
82 this.init_row(rows[r]);
87 this.frame = this.list.parentNode;
91 rcube_event.add_listener({event:bw.opera?'keypress':'keydown', object:this, method:'key_press'});
92 rcube_event.add_listener({event:'keydown', object:this, method:'key_down'});
99 * Init list row and set mouse events on it
101 init_row: function(row)
103 // make references in internal array and set event handlers
104 if (row && String(row.id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i)) {
108 this.rows[uid] = {uid:uid, id:row.id, obj:row};
110 // set eventhandlers to table row
111 row.onmousedown = function(e){ return self.drag_row(e, this.uid); };
112 row.onmouseup = function(e){ return self.click_row(e, this.uid); };
114 if (bw.iphone || bw.ipad) {
115 row.addEventListener('touchstart', function(e) {
116 if (e.touches.length == 1) {
117 if (!self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid))
121 row.addEventListener('touchend', function(e) {
122 if (e.changedTouches.length == 1)
123 if (!self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
129 row.onselectstart = function() { return false; };
131 this.row_init(this.rows[uid]);
137 * Init list column headers and set mouse events on them
139 init_header: function()
141 if (this.list && this.list.tHead) {
144 var col, r, p = this;
145 // add events for list columns moving
146 if (this.column_movable && this.list.tHead && this.list.tHead.rows) {
147 for (r=0; r<this.list.tHead.rows[0].cells.length; r++) {
148 if (this.column_fixed == r)
150 col = this.list.tHead.rows[0].cells[r];
151 col.onmousedown = function(e){ return p.drag_column(e, this); };
160 * Remove all list rows
164 var tbody = document.createElement('tbody');
166 this.list.insertBefore(tbody, this.list.tBodies[0]);
167 this.list.removeChild(this.list.tBodies[1]);
172 this.clear_selection();
174 // reset scroll position (in Opera)
176 this.frame.scrollTop = 0;
181 * 'remove' message row from list (just hide it)
183 remove_row: function(uid, sel_next)
185 if (this.rows[uid].obj)
186 this.rows[uid].obj.style.display = 'none';
191 delete this.rows[uid];
197 * Add row to the list and initialize it
199 insert_row: function(row, attop)
201 var tbody = this.list.tBodies[0];
203 if (attop && tbody.rows.length)
204 tbody.insertBefore(row, tbody.firstChild);
206 tbody.appendChild(row);
215 * Set focus to the list
222 for (n in this.selection) {
223 id = this.selection[n];
224 if (this.rows[id] && this.rows[id].obj) {
225 $(this.rows[id].obj).addClass('selected').removeClass('unfocused');
229 // Un-focus already focused elements
230 $('*:focus', window).blur();
231 $('iframe').each(function() { this.blur(); });
233 if (e || (e = window.event))
234 rcube_event.cancel(e);
239 * remove focus from the list
244 this.focused = false;
245 for (n in this.selection) {
246 id = this.selection[n];
247 if (this.rows[id] && this.rows[id].obj) {
248 $(this.rows[id].obj).removeClass('selected').addClass('unfocused');
255 * onmousedown-handler of message list column
257 drag_column: function(e, col)
259 if (this.colcount > 1) {
260 this.drag_start = true;
261 this.drag_mouse_start = rcube_event.get_mouse_pos(e);
263 rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
264 rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
266 // enable dragging over iframes
269 // find selected column number
270 for (var i=0; i<this.list.tHead.rows[0].cells.length; i++) {
271 if (col == this.list.tHead.rows[0].cells[i]) {
272 this.selected_column = i;
283 * onmousedown-handler of message list row
285 drag_row: function(e, id)
287 // don't do anything (another action processed before)
288 var evtarget = rcube_event.get_target(e),
289 tagname = evtarget.tagName.toLowerCase();
291 if (this.dont_select || (evtarget && (tagname == 'input' || tagname == 'img')))
294 // accept right-clicks
295 if (rcube_event.get_button(e) == 2)
298 this.in_selection_before = this.in_selection(id) ? id : false;
300 // selects currently unselected row
301 if (!this.in_selection_before) {
302 var mod_key = rcube_event.get_modifier(e);
303 this.select_row(id, mod_key, false);
306 if (this.draggable && this.selection.length) {
307 this.drag_start = true;
308 this.drag_mouse_start = rcube_event.get_mouse_pos(e);
309 rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
310 rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
311 if (bw.iphone || bw.ipad) {
312 rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
313 rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
316 // enable dragging over iframes
325 * onmouseup-handler of message list row
327 click_row: function(e, id)
329 var now = new Date().getTime(),
330 mod_key = rcube_event.get_modifier(e),
331 evtarget = rcube_event.get_target(e),
332 tagname = evtarget.tagName.toLowerCase();
334 if ((evtarget && (tagname == 'input' || tagname == 'img')))
337 // don't do anything (another action processed before)
338 if (this.dont_select) {
339 this.dont_select = false;
343 var dblclicked = now - this.rows[id].clicked < this.dblclick_time;
345 // unselects currently selected row
346 if (!this.drag_active && this.in_selection_before == id && !dblclicked)
347 this.select_row(id, mod_key, false);
349 this.drag_start = false;
350 this.in_selection_before = false;
352 // row was double clicked
353 if (this.rows && dblclicked && this.in_selection(id))
354 this.triggerEvent('dblclick');
356 this.triggerEvent('click');
358 if (!this.drag_active) {
361 rcube_event.cancel(e);
364 this.rows[id].clicked = now;
370 * Returns thread root ID for specified row ID
372 find_root: function(uid)
374 var r = this.rows[uid];
376 if (r && r.parent_uid)
377 return this.find_root(r.parent_uid);
383 expand_row: function(e, id)
385 var row = this.rows[id],
386 evtarget = rcube_event.get_target(e),
387 mod_key = rcube_event.get_modifier(e);
389 // Don't select this message
390 this.dont_select = true;
391 // Don't treat double click on the expando as double click on the message.
395 evtarget.className = 'collapsed';
396 if (mod_key == CONTROL_KEY || this.multiexpand)
397 this.collapse_all(row);
402 evtarget.className = 'expanded';
403 if (mod_key == CONTROL_KEY || this.multiexpand)
404 this.expand_all(row);
410 collapse: function(row)
412 row.expanded = false;
413 this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
414 var depth = row.depth;
415 var new_row = row ? row.obj.nextSibling : null;
419 if (new_row.nodeType == 1) {
420 var r = this.rows[new_row.uid];
421 if (r && r.depth <= depth)
423 $(new_row).css('display', 'none');
426 this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
429 new_row = new_row.nextSibling;
435 expand: function(row)
437 var r, p, depth, new_row, last_expanded_parent_depth;
442 new_row = row.obj.nextSibling;
443 this.update_expando(row.uid, true);
444 this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
447 var tbody = this.list.tBodies[0];
448 new_row = tbody.firstChild;
450 last_expanded_parent_depth = 0;
454 if (new_row.nodeType == 1) {
455 r = this.rows[new_row.uid];
457 if (row && (!r.depth || r.depth <= depth))
461 p = this.rows[r.parent_uid];
462 if (p && p.expanded) {
463 if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
464 last_expanded_parent_depth = p.depth;
465 $(new_row).css('display', '');
467 this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
471 if (row && (! p || p.depth <= depth))
476 new_row = new_row.nextSibling;
483 collapse_all: function(row)
485 var depth, new_row, r;
488 row.expanded = false;
490 new_row = row.obj.nextSibling;
491 this.update_expando(row.uid);
492 this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
494 // don't collapse sub-root tree in multiexpand mode
495 if (depth && this.multiexpand)
499 new_row = this.list.tBodies[0].firstChild;
504 if (new_row.nodeType == 1) {
505 if (r = this.rows[new_row.uid]) {
506 if (row && (!r.depth || r.depth <= depth))
510 $(new_row).css('display', 'none');
511 if (r.has_children && r.expanded) {
513 this.update_expando(r.uid, false);
514 this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
518 new_row = new_row.nextSibling;
524 expand_all: function(row)
526 var depth, new_row, r;
531 new_row = row.obj.nextSibling;
532 this.update_expando(row.uid, true);
533 this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded });
536 new_row = this.list.tBodies[0].firstChild;
541 if (new_row.nodeType == 1) {
542 if (r = this.rows[new_row.uid]) {
543 if (row && r.depth <= depth)
546 $(new_row).css('display', '');
547 if (r.has_children && !r.expanded) {
549 this.update_expando(r.uid, true);
550 this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded });
554 new_row = new_row.nextSibling;
559 update_expando: function(uid, expanded)
561 var expando = document.getElementById('rcmexpando' + uid);
563 expando.className = expanded ? 'expanded' : 'collapsed';
568 * get first/next/previous/last rows that are not hidden
570 get_next_row: function()
575 var last_selected_row = this.rows[this.last_selected],
576 new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
578 while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
579 new_row = new_row.nextSibling;
584 get_prev_row: function()
589 var last_selected_row = this.rows[this.last_selected],
590 new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
592 while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
593 new_row = new_row.previousSibling;
598 get_first_row: function()
601 var i, len, rows = this.list.tBodies[0].rows;
603 for (i=0, len=rows.length-1; i<len; i++)
604 if (rows[i].id && String(rows[i].id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i) && this.rows[RegExp.$1] != null)
611 get_last_row: function()
614 var i, rows = this.list.tBodies[0].rows;
616 for (i=rows.length-1; i>=0; i--)
617 if (rows[i].id && String(rows[i].id).match(/^rcmrow([a-z0-9\-_=\+\/]+)/i) && this.rows[RegExp.$1] != null)
626 * selects or unselects the proper row depending on the modifier key pressed
628 select_row: function(id, mod_key, with_mouse)
630 var select_before = this.selection.join(',');
631 if (!this.multiselect)
634 if (!this.shift_start)
635 this.shift_start = id
638 this.shift_start = id;
639 this.highlight_row(id, false);
640 this.multi_selecting = false;
645 this.shift_select(id, false);
650 this.highlight_row(id, true);
653 case CONTROL_SHIFT_KEY:
654 this.shift_select(id, true);
658 this.highlight_row(id, false);
661 this.multi_selecting = true;
664 // trigger event if selection changed
665 if (this.selection.join(',') != select_before)
666 this.triggerEvent('select');
668 if (this.last_selected != 0 && this.rows[this.last_selected])
669 $(this.rows[this.last_selected].obj).removeClass('focused');
671 // unselect if toggleselect is active and the same row was clicked again
672 if (this.toggleselect && this.last_selected == id) {
673 this.clear_selection();
677 $(this.rows[id].obj).addClass('focused');
679 if (!this.selection.length)
680 this.shift_start = null;
682 this.last_selected = id;
687 * Alias method for select_row
691 this.select_row(id, false);
697 * Select row next to the last selected one.
698 * Either below or above.
700 select_next: function()
702 var next_row = this.get_next_row(),
703 prev_row = this.get_prev_row(),
704 new_row = (next_row) ? next_row : prev_row;
707 this.select_row(new_row.uid, false, false);
714 select_first: function(mod_key)
716 var row = this.get_first_row();
719 this.shift_select(row, mod_key);
720 this.triggerEvent('select');
733 select_last: function(mod_key)
735 var row = this.get_last_row();
738 this.shift_select(row, mod_key);
739 this.triggerEvent('select');
750 * Add all childs of the given row to selection
752 select_childs: function(uid)
754 if (!this.rows[uid] || !this.rows[uid].has_children)
757 var depth = this.rows[uid].depth,
758 row = this.rows[uid].obj.nextSibling;
761 if (row.nodeType == 1) {
762 if ((r = this.rows[row.uid])) {
763 if (!r.depth || r.depth <= depth)
765 if (!this.in_selection(r.uid))
766 this.select_row(r.uid, CONTROL_KEY);
769 row = row.nextSibling;
775 * Perform selection when shift key is pressed
777 shift_select: function(id, control)
779 if (!this.rows[this.shift_start] || !this.selection.length)
780 this.shift_start = id;
782 var n, from_rowIndex = this.rows[this.shift_start].obj.rowIndex,
783 to_rowIndex = this.rows[id].obj.rowIndex,
784 i = ((from_rowIndex < to_rowIndex)? from_rowIndex : to_rowIndex),
785 j = ((from_rowIndex > to_rowIndex)? from_rowIndex : to_rowIndex);
787 // iterate through the entire message list
788 for (n in this.rows) {
789 if (this.rows[n].obj.rowIndex >= i && this.rows[n].obj.rowIndex <= j) {
790 if (!this.in_selection(n)) {
791 this.highlight_row(n, true);
795 if (this.in_selection(n) && !control) {
796 this.highlight_row(n, true);
804 * Check if given id is part of the current selection
806 in_selection: function(id)
808 for (var n in this.selection)
809 if (this.selection[n]==id)
817 * Select each row in list
819 select_all: function(filter)
821 if (!this.rows || !this.rows.length)
824 // reset but remember selection first
825 var n, select_before = this.selection.join(',');
828 for (n in this.rows) {
829 if (!filter || this.rows[n][filter] == true) {
830 this.last_selected = n;
831 this.highlight_row(n, true);
834 $(this.rows[n].obj).removeClass('selected').removeClass('unfocused');
838 // trigger event if selection changed
839 if (this.selection.join(',') != select_before)
840 this.triggerEvent('select');
851 invert_selection: function()
853 if (!this.rows || !this.rows.length)
856 // remember old selection
857 var n, select_before = this.selection.join(',');
860 this.highlight_row(n, true);
862 // trigger event if selection changed
863 if (this.selection.join(',') != select_before)
864 this.triggerEvent('select');
873 * Unselect selected row(s)
875 clear_selection: function(id)
877 var n, num_select = this.selection.length;
881 for (n in this.selection)
882 if (this.selection[n] == id) {
883 this.selection.splice(n,1);
889 for (n in this.selection)
890 if (this.rows[this.selection[n]]) {
891 $(this.rows[this.selection[n]].obj).removeClass('selected').removeClass('unfocused');
897 if (num_select && !this.selection.length)
898 this.triggerEvent('select');
903 * Getter for the selection array
905 get_selection: function()
907 return this.selection;
912 * Return the ID if only one row is selected
914 get_single_selection: function()
916 if (this.selection.length == 1)
917 return this.selection[0];
924 * Highlight/unhighlight a row
926 highlight_row: function(id, multiple)
928 if (this.rows[id] && !multiple) {
929 if (this.selection.length > 1 || !this.in_selection(id)) {
930 this.clear_selection();
931 this.selection[0] = id;
932 $(this.rows[id].obj).addClass('selected');
935 else if (this.rows[id]) {
936 if (!this.in_selection(id)) { // select row
937 this.selection[this.selection.length] = id;
938 $(this.rows[id].obj).addClass('selected');
940 else { // unselect row
941 var p = $.inArray(id, this.selection),
942 a_pre = this.selection.slice(0, p),
943 a_post = this.selection.slice(p+1, this.selection.length);
945 this.selection = a_pre.concat(a_post);
946 $(this.rows[id].obj).removeClass('selected').removeClass('unfocused');
953 * Handler for keyboard events
955 key_press: function(e)
957 var target = e.target || {};
958 if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
961 var keyCode = rcube_event.get_keycode(e),
962 mod_key = rcube_event.get_modifier(e);
967 case 63233: // "down", in safari keypress
968 case 63232: // "up", in safari keypress
969 // Stop propagation so that the browser doesn't scroll
970 rcube_event.cancel(e);
971 return this.use_arrow_key(keyCode, mod_key);
973 case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2)
977 rcube_event.cancel(e);
978 var ret = this.use_plusminus_key(keyCode, mod_key);
979 this.key_pressed = keyCode;
980 this.modkey = mod_key;
981 this.triggerEvent('keypress');
985 this.select_first(mod_key);
986 return rcube_event.cancel(e);
988 this.select_last(mod_key);
989 return rcube_event.cancel(e);
991 this.key_pressed = keyCode;
992 this.modkey = mod_key;
993 this.triggerEvent('keypress');
996 if (this.key_pressed == this.BACKSPACE_KEY)
997 return rcube_event.cancel(e);
1004 * Handler for keydown events
1006 key_down: function(e)
1008 var target = e.target || {};
1009 if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
1012 switch (rcube_event.get_keycode(e)) {
1014 if (this.drag_active)
1015 return this.drag_mouse_up(e);
1016 if (this.col_drag_active) {
1017 this.selected_column = null;
1018 return this.column_drag_mouse_up(e);
1029 if (!rcube_event.get_modifier(e) && this.focused)
1030 return rcube_event.cancel(e);
1040 * Special handling method for arrow keys
1042 use_arrow_key: function(keyCode, mod_key)
1045 // Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
1046 // using the keypress event (but not the keydown or keyup event).
1047 if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
1048 new_row = this.get_next_row();
1049 else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
1050 new_row = this.get_prev_row();
1053 this.select_row(new_row.uid, mod_key, false);
1054 this.scrollto(new_row.uid);
1062 * Special handling method for +/- keys
1064 use_plusminus_key: function(keyCode, mod_key)
1066 var selected_row = this.rows[this.last_selected];
1071 keyCode = selected_row.expanded ? 109 : 61;
1072 if (keyCode == 61 || keyCode == 107)
1073 if (mod_key == CONTROL_KEY || this.multiexpand)
1074 this.expand_all(selected_row);
1076 this.expand(selected_row);
1078 if (mod_key == CONTROL_KEY || this.multiexpand)
1079 this.collapse_all(selected_row);
1081 this.collapse(selected_row);
1083 this.update_expando(selected_row.uid, selected_row.expanded);
1090 * Try to scroll the list to make the specified row visible
1092 scrollto: function(id)
1094 var row = this.rows[id].obj;
1095 if (row && this.frame) {
1096 var scroll_to = Number(row.offsetTop);
1098 // expand thread if target row is hidden (collapsed)
1099 if (!scroll_to && this.rows[id].parent_uid) {
1100 var parent = this.find_root(this.rows[id].uid);
1101 this.expand_all(this.rows[parent]);
1102 scroll_to = Number(row.offsetTop);
1105 if (scroll_to < Number(this.frame.scrollTop))
1106 this.frame.scrollTop = scroll_to;
1107 else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
1108 this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
1114 * Handler for mouse move events
1116 drag_mouse_move: function(e)
1118 // convert touch event
1119 if (e.type == 'touchmove') {
1120 if (e.changedTouches.length == 1)
1121 e = rcube_event.touchevent(e.changedTouches[0]);
1123 return rcube_event.cancel(e);
1126 if (this.drag_start) {
1127 // check mouse movement, of less than 3 pixels, don't start dragging
1128 var m = rcube_event.get_mouse_pos(e);
1130 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))
1133 if (!this.draglayer)
1134 this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
1135 .css({ position:'absolute', display:'none', 'z-index':2000 })
1136 .appendTo(document.body);
1138 // also select childs of (collapsed) threads for dragging
1139 var n, uid, selection = $.merge([], this.selection);
1140 for (n in selection) {
1142 if (this.rows[uid].has_children && !this.rows[uid].expanded)
1143 this.select_childs(uid);
1147 this.draglayer.html('');
1149 // get subjects of selected messages
1150 var c, i, n, subject, obj;
1151 for (n=0; n<this.selection.length; n++) {
1152 // only show 12 lines
1154 this.draglayer.append('...');
1158 if (obj = this.rows[this.selection[n]].obj) {
1161 for (c=0, i=0; i<obj.childNodes.length; i++) {
1162 if (obj.childNodes[i].nodeName == 'TD') {
1164 this.drag_start_pos = $(obj.childNodes[i]).offset();
1166 if (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)) {
1167 var entry, node, tmp_node, nodes = obj.childNodes[i].childNodes;
1169 for (m=0; m<nodes.length; m++) {
1170 if ((tmp_node = obj.childNodes[i].childNodes[m]) && (tmp_node.nodeType==3 || tmp_node.nodeName=='A'))
1177 subject = $(node).text();
1178 // remove leading spaces
1179 subject = $.trim(subject);
1180 // truncate line to 50 characters
1181 subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
1183 entry = $('<div>').text(subject);
1184 this.draglayer.append(entry);
1193 this.draglayer.show();
1194 this.drag_active = true;
1195 this.triggerEvent('dragstart');
1198 if (this.drag_active && this.draglayer) {
1199 var pos = rcube_event.get_mouse_pos(e);
1200 this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
1201 this.triggerEvent('dragmove', e?e:window.event);
1204 this.drag_start = false;
1211 * Handler for mouse up events
1213 drag_mouse_up: function(e)
1215 document.onmousemove = null;
1217 if (e.type == 'touchend') {
1218 if (e.changedTouches.length != 1)
1219 return rcube_event.cancel(e);
1222 if (this.draglayer && this.draglayer.is(':visible')) {
1223 if (this.drag_start_pos)
1224 this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
1226 this.draglayer.hide();
1229 if (this.drag_active)
1231 this.drag_active = false;
1233 rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
1234 rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
1236 if (bw.iphone || bw.ipad) {
1237 rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
1238 rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
1244 this.triggerEvent('dragend');
1246 return rcube_event.cancel(e);
1251 * Handler for mouse move events for dragging list column
1253 column_drag_mouse_move: function(e)
1255 if (this.drag_start) {
1256 // check mouse movement, of less than 3 pixels, don't start dragging
1257 var i, m = rcube_event.get_mouse_pos(e);
1259 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))
1262 if (!this.col_draglayer) {
1263 var lpos = $(this.list).offset(),
1264 cells = this.list.tHead.rows[0].cells;
1266 // create dragging layer
1267 this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
1268 .css(lpos).css({ position:'absolute', 'z-index':2001,
1269 'background-color':'white', opacity:0.75,
1270 height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
1271 .appendTo(document.body)
1272 // ... and column position indicator
1273 .append($('<div>').attr('id', 'rcmcolumnindicator')
1274 .css({ position:'absolute', 'border-right':'2px dotted #555',
1275 'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
1278 this.list_pos = this.list_min_pos = lpos.left;
1279 // save columns positions
1280 for (i=0; i<cells.length; i++) {
1281 this.cols[i] = cells[i].offsetWidth;
1282 if (this.column_fixed !== null && i <= this.column_fixed) {
1283 this.list_min_pos += this.cols[i];
1288 this.col_draglayer.show();
1289 this.col_drag_active = true;
1290 this.triggerEvent('column_dragstart');
1293 // set column indicator position
1294 if (this.col_drag_active && this.col_draglayer) {
1295 var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
1297 for (i=0; i<this.cols.length; i++) {
1298 if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
1299 cpos += this.cols[i];
1304 // handle fixed columns on left
1305 if (i == 0 && this.list_min_pos > pos.x)
1306 cpos = this.list_min_pos - this.list_pos;
1307 // empty list needs some assignment
1308 else if (!this.list.rowcount && i == this.cols.length)
1310 $('#rcmcolumnindicator').css({ width: cpos+'px'});
1311 this.triggerEvent('column_dragmove', e?e:window.event);
1314 this.drag_start = false;
1321 * Handler for mouse up events for dragging list columns
1323 column_drag_mouse_up: function(e)
1325 document.onmousemove = null;
1327 if (this.col_draglayer) {
1328 (this.col_draglayer).remove();
1329 this.col_draglayer = null;
1332 if (this.col_drag_active)
1334 this.col_drag_active = false;
1336 rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
1337 rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
1341 if (this.selected_column !== null && this.cols && this.cols.length) {
1342 var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
1344 // find destination position
1345 for (i=0; i<this.cols.length; i++) {
1346 if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
1347 cpos += this.cols[i];
1352 if (i != this.selected_column && i != this.selected_column+1) {
1353 this.column_replace(this.selected_column, i);
1357 this.triggerEvent('column_dragend');
1359 return rcube_event.cancel(e);
1364 * Creates a layer for drag&drop over iframes
1366 add_dragfix: function()
1368 $('iframe').each(function() {
1369 $('<div class="iframe-dragdrop-fix"></div>')
1370 .css({background: '#fff',
1371 width: this.offsetWidth+'px', height: this.offsetHeight+'px',
1372 position: 'absolute', opacity: '0.001', zIndex: 1000
1374 .css($(this).offset())
1375 .appendTo(document.body);
1381 * Removes the layer for drag&drop over iframes
1383 del_dragfix: function()
1385 $('div.iframe-dragdrop-fix').each(function() { this.parentNode.removeChild(this); });
1390 * Replaces two columns
1392 column_replace: function(from, to)
1394 var len, cells = this.list.tHead.rows[0].cells,
1397 td = document.createElement('td');
1399 // replace header cells
1401 cells[0].parentNode.insertBefore(td, before);
1403 cells[0].parentNode.appendChild(td);
1404 cells[0].parentNode.replaceChild(elem, td);
1406 // replace list cells
1407 for (r=0, len=this.list.tBodies[0].rows.length; r<len; r++) {
1408 row = this.list.tBodies[0].rows[r];
1410 elem = row.cells[from];
1411 before = row.cells[to];
1412 td = document.createElement('td');
1415 row.insertBefore(td, before);
1417 row.appendChild(td);
1418 row.replaceChild(elem, td);
1421 // update subject column position
1422 if (this.subject_col == from)
1423 this.subject_col = to > from ? to - 1 : to;
1424 else if (this.subject_col < from && to <= this.subject_col)
1426 else if (this.subject_col > from && to >= this.subject_col)
1429 this.triggerEvent('column_replace');
1434 rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
1435 rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
1436 rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;