From bc2accc455e1dab67e0d29f034deddd9401ecad2 Mon Sep 17 00:00:00 2001 From: alecpl <alec@alec.pl> Date: Thu, 25 Mar 2010 14:16:50 -0400 Subject: [PATCH] - Added Home/End kayboard keys support on lists (#1486430) --- program/js/list.js | 405 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 files changed, 377 insertions(+), 28 deletions(-) diff --git a/program/js/list.js b/program/js/list.js index 193e467..13fa7d3 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -37,6 +37,7 @@ this.subject_col = -1; this.shiftkey = false; this.multiselect = false; + this.multiexpand = false; this.multi_selecting = false; this.draggable = false; this.keyboard = false; @@ -76,7 +77,7 @@ for(var r=0; r<this.list.tBodies[0].childNodes.length; r++) { row = this.list.tBodies[0].childNodes[r]; - while (row && (row.nodeType != 1 || row.style.display == 'none')) + while (row && row.nodeType != 1) { row = row.nextSibling; r++; @@ -108,7 +109,7 @@ var p = this; var uid = RegExp.$1; row.uid = uid; - this.rows[uid] = {uid:uid, id:row.id, obj:row, classname:row.className}; + this.rows[uid] = {uid:uid, id:row.id, obj:row}; // set eventhandlers to table row row.onmousedown = function(e){ return p.drag_row(e, this.uid); }; @@ -127,7 +128,7 @@ */ clear: function(sel) { - var tbody = document.createElement('TBODY'); + var tbody = document.createElement('tbody'); this.list.insertBefore(tbody, this.list.tBodies[0]); this.list.removeChild(this.list.tBodies[1]); this.rows = new Array(); @@ -217,7 +218,8 @@ { // don't do anything (another action processed before) var evtarget = rcube_event.get_target(e); - if (this.dont_select || (evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG'))) + var tagname = evtarget.tagName.toLowerCase(); + if (this.dont_select || (evtarget && (tagname == 'input' || tagname == 'img'))) return true; // accept right-clicks @@ -241,7 +243,7 @@ rcube_event.add_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'}); // add listener for iframes - var iframes = document.getElementsByTagName('IFRAME'); + var iframes = document.getElementsByTagName('iframe'); this.iframe_events = Object(); for (var n in iframes) { @@ -283,8 +285,9 @@ var now = new Date().getTime(); var mod_key = rcube_event.get_modifier(e); var evtarget = rcube_event.get_target(e); + var tagname = evtarget.tagName.toLowerCase(); - if ((evtarget && (evtarget.tagName == 'INPUT' || evtarget.tagName == 'IMG'))) + if ((evtarget && (tagname == 'input' || tagname == 'img'))) return true; // don't do anything (another action processed before) @@ -317,8 +320,211 @@ }, +/* + * Returns thread root ID for specified row ID + */ +find_root: function(uid) +{ + var r = this.rows[uid]; + + if (r && r.parent_uid) + return this.find_root(r.parent_uid); + else + return uid; +}, + + +expand_row: function(e, id) +{ + var row = this.rows[id]; + var evtarget = rcube_event.get_target(e); + var mod_key = rcube_event.get_modifier(e); + + // Don't select this message + this.dont_select = true; + // Don't treat double click on the expando as double click on the message. + row.clicked = 0; + + if (row.expanded) { + evtarget.className = "collapsed"; + if (mod_key == CONTROL_KEY || this.multiexpand) + this.collapse_all(row); + else + this.collapse(row); + } + else { + evtarget.className = "expanded"; + if (mod_key == CONTROL_KEY || this.multiexpand) + this.expand_all(row); + else + this.expand(row); + } +}, + +collapse: function(row) +{ + row.expanded = false; + this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded }); + var depth = row.depth; + var new_row = row ? row.obj.nextSibling : null; + var r; + + while (new_row) { + if (new_row.nodeType == 1) { + var r = this.rows[new_row.uid]; + if (r && r.depth <= depth) + break; + $(new_row).hide(); + r.expanded = false; + this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded }); + } + new_row = new_row.nextSibling; + } + + return false; +}, + +expand: function(row) +{ + var depth, new_row; + var last_expanded_parent_depth; + + if (row) { + row.expanded = true; + depth = row.depth; + new_row = row.obj.nextSibling; + this.update_expando(row.uid, true); + this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded }); + } + else { + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + depth = 0; + last_expanded_parent_depth = 0; + } + + while (new_row) { + if (new_row.nodeType == 1) { + var r = this.rows[new_row.uid]; + if (r) { + if (row && (!r.depth || r.depth <= depth)) + break; + + if (r.parent_uid) { + var p = this.rows[r.parent_uid]; + if (p && p.expanded) { + if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) { + last_expanded_parent_depth = p.depth; + $(new_row).show(); + r.expanded = true; + this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded }); + } + } + else + if (row && (! p || p.depth <= depth)) + break; + } + } + } + new_row = new_row.nextSibling; + } + + return false; +}, + + +collapse_all: function(row) +{ + var depth, new_row; + var r; + + if (row) { + row.expanded = false; + depth = row.depth; + new_row = row.obj.nextSibling; + this.update_expando(row.uid); + this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded }); + + // don't collapse sub-root tree in multiexpand mode + if (depth && this.multiexpand) + return false; + } + else { + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + depth = 0; + } + + while (new_row) { + if (new_row.nodeType == 1) { + var r = this.rows[new_row.uid]; + if (r) { + if (row && (!r.depth || r.depth <= depth)) + break; + + if (row || r.depth) + $(new_row).hide(); + if (r.has_children) { + r.expanded = false; + this.update_expando(r.uid); + this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded }); + } + } + } + new_row = new_row.nextSibling; + } + + return false; +}, + +expand_all: function(row) +{ + var depth, new_row; + var r; + + if (row) { + row.expanded = true; + depth = row.depth; + new_row = row.obj.nextSibling; + this.update_expando(row.uid, true); + this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded }); + } + else { + var tbody = this.list.tBodies[0]; + new_row = tbody.firstChild; + depth = 0; + } + + while (new_row) { + if (new_row.nodeType == 1) { + var r = this.rows[new_row.uid]; + if (r) { + if (row && r.depth <= depth) + break; + + $(new_row).show(); + if (r.has_children) { + r.expanded = true; + this.update_expando(r.uid, true); + this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded }); + } + } + } + new_row = new_row.nextSibling; + } + return false; +}, + +update_expando: function(uid, expanded) +{ + var expando = document.getElementById('rcmexpando' + uid); + if (expando) + expando.className = expanded ? 'expanded' : 'collapsed'; +}, + + /** - * get next/previous/last rows that are not hidden + * get first/next/previous/last rows that are not hidden */ get_next_row: function() { @@ -344,6 +550,20 @@ new_row = new_row.previousSibling; return new_row; +}, + +get_first_row: function() +{ + if (this.rowcount) + { + var rows = this.list.tBodies[0].rows; + + for(var i=0; i<rows.length-1; i++) + if(rows[i].id && String(rows[i].id).match(/rcmrow([a-z0-9\-_=\+\/]+)/i) && this.rows[RegExp.$1] != null) + return RegExp.$1; + } + + return null; }, get_last_row: function() @@ -451,6 +671,62 @@ /** + * Select first row + */ +select_first: function(mod_key) +{ + var row = this.get_first_row(); + if (row && mod_key) { + this.shift_select(row, mod_key); + this.triggerEvent('select'); + this.scrollto(row); + } + else if (row) + this.select(row); +}, + + +/** + * Select last row + */ +select_last: function(mod_key) +{ + var row = this.get_last_row(); + if (row && mod_key) { + this.shift_select(row, mod_key); + this.triggerEvent('select'); + this.scrollto(row); + } + else if (row) + this.select(row); +}, + + +/** + * Add all childs of the given row to selection + */ +select_childs: function(uid) +{ + if (!this.rows[uid] || !this.rows[uid].has_children) + return; + + var depth = this.rows[uid].depth; + var row = this.rows[uid].obj.nextSibling; + while (row) { + if (row.nodeType == 1) { + if ((r = this.rows[row.uid])) { + if (!r.depth || r.depth <= depth) + break; + if (!this.in_selection(r.uid)) + this.select_row(r.uid, CONTROL_KEY); + } + } + row = row.nextSibling; + } +}, + + +/** * Perform selection when shift key is pressed */ shift_select: function(id, control) @@ -469,13 +745,15 @@ { if ((this.rows[n].obj.rowIndex >= i) && (this.rows[n].obj.rowIndex <= j)) { - if (!this.in_selection(n)) + if (!this.in_selection(n)) { this.highlight_row(n, true); + } } else { - if (this.in_selection(n) && !control) + if (this.in_selection(n) && !control) { this.highlight_row(n, true); + } } } }, @@ -490,7 +768,7 @@ if (this.selection[n]==id) return true; - return false; + return false; }, @@ -541,7 +819,7 @@ var select_before = this.selection.join(','); for (var n in this.rows) - this.highlight_row(n, true); + this.highlight_row(n, true); // trigger event if selection changed if (this.selection.join(',') != select_before) @@ -629,7 +907,7 @@ } else // unselect row { - var p = find_in_array(id, this.selection); + var p = jQuery.inArray(id, this.selection); var a_pre = this.selection.slice(0, p); var a_post = this.selection.slice(p+1, this.selection.length); this.selection = a_pre.concat(a_post); @@ -659,6 +937,22 @@ // Stop propagation so that the browser doesn't scroll rcube_event.cancel(e); return this.use_arrow_key(keyCode, mod_key); + case 61: + case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2) + case 109: + case 32: + // Stop propagation + rcube_event.cancel(e); + var ret = this.use_plusminus_key(keyCode, mod_key); + this.key_pressed = keyCode; + this.triggerEvent('keypress'); + return ret; + case 36: // Home + this.select_first(mod_key); + return rcube_event.cancel(e); + case 35: // End + this.select_last(mod_key); + return rcube_event.cancel(e); default: this.shiftkey = e.shiftKey; this.key_pressed = keyCode; @@ -686,6 +980,10 @@ case 38: case 63233: case 63232: + case 61: + case 107: + case 109: + case 32: if (!rcube_event.get_modifier(e) && this.focused) return rcube_event.cancel(e); @@ -720,6 +1018,36 @@ /** + * Special handling method for +/- keys + */ +use_plusminus_key: function(keyCode, mod_key) +{ + var selected_row = this.rows[this.last_selected]; + if (!selected_row) + return; + + if (keyCode == 32) + keyCode = selected_row.expanded ? 109 : 61; + if (keyCode == 61 || keyCode == 107) + if (mod_key == CONTROL_KEY || this.multiexpand) + this.expand_all(selected_row); + else + this.expand(selected_row); + else + if (mod_key == CONTROL_KEY || this.multiexpand) + this.collapse_all(selected_row); + else + this.collapse(selected_row); + + var expando = document.getElementById('rcmexpando' + selected_row.uid); + if (expando) + expando.className = selected_row.expanded?'expanded':'collapsed'; + + return false; +}, + + +/** * Try to scroll the list to make the specified row visible */ scrollto: function(id) @@ -728,6 +1056,13 @@ if (row && this.frame) { var scroll_to = Number(row.offsetTop); + + // expand thread if target row is hidden (collapsed) + if (!scroll_to && this.rows[id].parent_uid) { + var parent = this.find_root(this.rows[id].uid); + this.expand_all(this.rows[parent]); + scroll_to = Number(row.offsetTop); + } if (scroll_to < Number(this.frame.scrollTop)) this.frame.scrollTop = scroll_to; @@ -752,10 +1087,19 @@ if (!this.draglayer) this.draglayer = $('<div>').attr('id', 'rcmdraglayer').css({ position:'absolute', display:'none', 'z-index':2000 }).appendTo(document.body); + + // also select childs of (collapsed) threads for dragging + var selection = $.merge([], this.selection); + var depth, row, uid, r; + for (var n=0; n < selection.length; n++) { + uid = selection[n]; + if (this.rows[uid].has_children && !this.rows[uid].expanded) + this.select_childs(uid); + } - // get subjects of selectedd messages + // get subjects of selected messages var names = ''; - var c, i, node, subject, obj; + var c, i, subject, obj; for(var n=0; n<this.selection.length; n++) { if (n>12) // only show 12 lines @@ -764,24 +1108,29 @@ break; } - if (this.rows[this.selection[n]].obj) + if (obj = this.rows[this.selection[n]].obj) { - obj = this.rows[this.selection[n]].obj; subject = ''; - for(c=0, i=0; i<obj.childNodes.length; i++) + for (c=0, i=0; i<obj.childNodes.length; i++) { - if (obj.childNodes[i].nodeName == 'TD') + if (obj.childNodes[i].nodeName == 'TD') { - if (((node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) && - (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c))) - { - if (n == 0) { - if (node.nodeType == 3) - this.drag_start_pos = $(obj.childNodes[i]).offset(); - else - this.drag_start_pos = $(node).offset(); + if (n == 0) + this.drag_start_pos = $(obj.childNodes[i]).offset(); + + if (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)) + { + var node, tmp_node, nodes = obj.childNodes[i].childNodes; + // find text node + for (m=0; m<nodes.length; m++) { + if ((tmp_node = obj.childNodes[i].childNodes[m]) && (tmp_node.nodeType==3 || tmp_node.nodeName=='A')) + node = tmp_node; } + + if (!node) + break; + subject = node.nodeType==3 ? node.data : node.innerHTML; // remove leading spaces subject = subject.replace(/^\s+/i, ''); @@ -835,7 +1184,7 @@ rcube_event.remove_listener({element:document, event:'mousemove', object:this, method:'drag_mouse_move'}); rcube_event.remove_listener({element:document, event:'mouseup', object:this, method:'drag_mouse_up'}); - var iframes = document.getElementsByTagName('IFRAME'); + var iframes = document.getElementsByTagName('iframe'); for (var n in iframes) { var iframedoc; @@ -869,7 +1218,7 @@ set_background_mode: function(flag) { if (flag) { - this.background = document.createElement('TBODY'); + this.background = document.createElement('tbody'); } else if (this.background) { this.list.replaceChild(this.background, this.list.tBodies[0]); this.background = null; -- Gitblit v1.9.1