Thomas Bruederli
2014-05-01 2ba491a0f93002424f7848c8a1726a03d857e381
program/js/list.js
@@ -1,21 +1,35 @@
/*
 +-----------------------------------------------------------------------+
 | Roundcube List Widget                                                 |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Authors: Thomas Bruederli <roundcube@gmail.com>                       |
 |          Charles McNulty <charles@charlesmcnulty.com>                 |
 +-----------------------------------------------------------------------+
 | Requires: common.js                                                   |
 +-----------------------------------------------------------------------+
*/
/**
 * Roundcube List Widget
 *
 * This file is part of the Roundcube Webmail client
 *
 * @licstart  The following is the entire license notice for the
 * JavaScript code in this file.
 *
 * Copyright (c) 2005-2014, The Roundcube Dev Team
 *
 * The JavaScript code in this page is free software: you can
 * redistribute it and/or modify it under the terms of the GNU
 * General Public License (GNU GPL) as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option)
 * any later version.  The code is distributed WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
 *
 * As additional permission under GNU GPL version 3 section 7, you
 * may distribute non-source (e.g., minimized or compacted) forms of
 * that code without the copy of the GNU GPL normally required by
 * section 4, provided you include this license notice and a URL
 * through which recipients can access the Corresponding Source.
 *
 * @licend  The above is the entire license notice
 * for the JavaScript code in this file.
 *
 * @author Thomas Bruederli <roundcube@gmail.com>
 * @author Charles McNulty <charles@charlesmcnulty.com>
 *
 * @requires jquery.js, common.js
 */
/**
@@ -84,7 +98,7 @@
    this.rows = {};
    this.rowcount = 0;
    var r, len, rows = this.tbody.childNodes;
    var r, len, rows = this.tbody.childNodes, me = this;
    for (r=0, len=rows.length; r<len; r++) {
      this.rowcount += this.init_row(rows[r]) ? 1 : 0;
@@ -94,8 +108,19 @@
    this.frame = this.list.parentNode;
    // set body events
    if (this.keyboard)
    if (this.keyboard) {
      rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
      // install a link element to receive focus.
      // this helps to maintain the natural tab order when moving focus with keyboard
      this.focus_elem = $('<a>')
        .attr('tabindex', '0')
        .attr('style', 'display:block; width:1px; height:1px; line-height:1px; overflow:hidden; position:absolute; top:-1000px')
        .html('Select List')
        .insertAfter(this.list)
        .on('focus', function(e){ me.focus(e); })
        .on('blur', function(e){ me.blur(e); });
    }
  }
  return this;
@@ -107,11 +132,11 @@
 */
init_row: function(row)
{
  row.uid = this.get_row_uid(row);
  // make references in internal array and set event handlers
  if (row && String(row.id).match(this.id_regexp)) {
    var self = this,
      uid = RegExp.$1;
    row.uid = uid;
  if (row && row.uid) {
    var self = this, uid = row.uid;
    this.rows[uid] = {uid:uid, id:row.id, obj:row};
    // set eventhandlers to table row
@@ -161,9 +186,9 @@
    if (this.fixed_header) {  // copy (modified) fixed header back to the actual table
      $(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
      $(this.list.tHead).find('tr td').attr('style', '');  // remove fixed widths
      $(this.list.tHead).find('tr td').attr('style', '').find('a.sortcol').attr('tabindex', '-1');  // remove fixed widths
    }
    else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
    else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0 && 0) {
      this.init_fixed_header();
    }
@@ -205,6 +230,12 @@
  else {
    $(this.fixed_header).find('thead').replaceWith(clone);
  }
  // avoid scrolling header links being focused
  $(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
  // set tabindex to fixed header sort links
  clone.find('a.sortcol').attr('tabindex', '0');
  this.thead = clone.get(0);
  this.resize();
@@ -251,6 +282,8 @@
  if (sel)
    this.clear_selection();
  else
    this.last_selected = 0;
  // reset scroll position (in Opera)
  if (this.frame)
@@ -299,12 +332,15 @@
    if (row.id) domrow.id = row.id;
    if (row.className) domrow.className = row.className;
    if (row.style) $.extend(domrow.style, row.style);
    if (row.uid) $(domrow).data('uid', row.uid);
    for (var domcell, col, i=0; row.cols && i < row.cols.length; i++) {
    for (var e, domcell, col, i=0; row.cols && i < row.cols.length; i++) {
      col = row.cols[i];
      domcell = document.createElement(this.col_tagname());
      if (col.className) domcell.className = col.className;
      if (col.innerHTML) domcell.innerHTML = col.innerHTML;
      for (e in col.events)
        domcell['on' + e] = col.events[e];
      domrow.appendChild(domcell);
    }
@@ -353,6 +389,9 @@
 */
focus: function(e)
{
  if (this.focused)
    return;
  var n, id;
  this.focused = true;
@@ -363,20 +402,32 @@
    }
  }
  // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
  // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
  $('iframe,:focus:not(body)').blur();
  window.focus();
  if (e || (e = window.event))
  if (e)
    rcube_event.cancel(e);
  // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
  if (this.focus_elem) {
    // We now fix this by explicitly assigning focus to a dedicated link element
    this.focus_elem.focus();
  }
  else {
    // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
    $('iframe,:focus:not(body)').blur();
    window.focus();
  }
  $(this.list).addClass('focus');
  // set internal focus pointer to first row
  if (!this.last_selected)
    this.select_first(CONTROL_KEY);
},
/**
 * remove focus from the list
 */
blur: function()
blur: function(e)
{
  var n, id;
  this.focused = false;
@@ -386,6 +437,22 @@
      $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused');
    }
  }
  $(this.list).removeClass('focus');
},
/**
 * Set/unset the given column as hidden
 */
hide_column: function(col, hide)
{
  var method = hide ? 'addClass' : 'removeClass';
  if (this.fixed_header)
    $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
  $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
},
@@ -583,7 +650,7 @@
    row.expanded = true;
    depth = row.depth;
    new_row = row.obj.nextSibling;
    this.update_expando(row.uid, true);
    this.update_expando(row.id, true);
    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  }
  else {
@@ -633,7 +700,7 @@
    row.expanded = false;
    depth = row.depth;
    new_row = row.obj.nextSibling;
    this.update_expando(row.uid);
    this.update_expando(row.id);
    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
    // don't collapse sub-root tree in multiexpand mode 
@@ -655,7 +722,7 @@
          $(new_row).css('display', 'none');
        if (r.has_children && r.expanded) {
          r.expanded = false;
          this.update_expando(r.uid, false);
          this.update_expando(r.id, false);
          this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
        }
      }
@@ -677,7 +744,7 @@
    row.expanded = true;
    depth = row.depth;
    new_row = row.obj.nextSibling;
    this.update_expando(row.uid, true);
    this.update_expando(row.id, true);
    this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
  }
  else {
@@ -694,7 +761,7 @@
        $(new_row).css('display', '');
        if (r.has_children && !r.expanded) {
          r.expanded = true;
          this.update_expando(r.uid, true);
          this.update_expando(r.id, true);
          this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
        }
      }
@@ -708,13 +775,26 @@
},
update_expando: function(uid, expanded)
update_expando: function(id, expanded)
{
  var expando = document.getElementById('rcmexpando' + uid);
  var expando = document.getElementById('rcmexpando' + id);
  if (expando)
    expando.className = expanded ? 'expanded' : 'collapsed';
},
get_row_uid: function(row)
{
  if (row && row.uid)
    return row.uid;
  var uid;
  if (row && (uid = $(row).data('uid')))
    row.uid = uid;
  else if (row && String(row.id).match(this.id_regexp))
    row.uid = RegExp.$1;
  return row.uid;
},
/**
 * get first/next/previous/last rows that are not hidden
@@ -750,11 +830,11 @@
get_first_row: function()
{
  if (this.rowcount) {
    var i, len, rows = this.tbody.childNodes;
    var i, len, uid, rows = this.tbody.childNodes;
    for (i=0, len=rows.length-1; i<len; i++)
      if (rows[i].id && String(rows[i].id).match(this.id_regexp) && this.rows[RegExp.$1] != null)
        return RegExp.$1;
      if (rows[i].id && (uid = this.get_row_uid(rows[i])))
        return uid;
  }
  return null;
@@ -763,11 +843,11 @@
get_last_row: function()
{
  if (this.rowcount) {
    var i, rows = this.tbody.childNodes;
    var i, uid, rows = this.tbody.childNodes;
    for (i=rows.length-1; i>=0; i--)
      if (rows[i].id && String(rows[i].id).match(this.id_regexp) && this.rows[RegExp.$1] != null)
        return RegExp.$1;
      if (rows[i].id && (uid = this.get_row_uid(rows[i])))
        return uid;
  }
  return null;
@@ -1035,7 +1115,7 @@
/**
 * Unselect selected row(s)
 */
clear_selection: function(id)
clear_selection: function(id, no_event)
{
  var n, num_select = this.selection.length;
@@ -1057,8 +1137,10 @@
    this.selection = [];
  }
  if (num_select && !this.selection.length)
  if (num_select && !this.selection.length && !no_event) {
    this.triggerEvent('select');
    this.last_selected = 0;
  }
},
@@ -1110,7 +1192,7 @@
  if (!multiple) {
    if (this.selection.length > 1 || !this.in_selection(id)) {
      this.clear_selection();
      this.clear_selection(null, true);
      this.selection[0] = id;
      $(this.rows[id].obj).addClass('selected');
    }
@@ -1261,14 +1343,22 @@
        this.collapse(selected_row);
    }
    this.update_expando(selected_row.uid, selected_row.expanded);
    this.update_expando(selected_row.id, selected_row.expanded);
    return false;
  }
  if (new_row) {
    // simulate ctr-key if no rows are selected
    if (!mod_key && !this.selection.length)
      mod_key = CONTROL_KEY;
    this.select_row(new_row.uid, mod_key, false);
    this.scrollto(new_row.uid);
  }
  else if (!new_row && !selected_row) {
    // select the first row if none selected yet
    this.select_first(CONTROL_KEY);
  }
  return false;
@@ -1340,12 +1430,9 @@
    // get selected rows (in display order), don't use this.selection here
    $(this.row_tagname() + '.selected', this.tbody).each(function() {
      if (!String(this.id).match(self.id_regexp))
        return;
      var uid = self.get_row_uid(this), row = self.rows[uid];
      var uid = RegExp.$1, row = self.rows[uid];
      if ($.inArray(uid, selection) > -1)
      if (!row || $.inArray(uid, selection) > -1)
        return;
      selection.push(uid);