Aleksander Machniak
2016-05-20 e48f8945b32ab5b67f1cdeb53a37d3d196e31e4d
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
 */
/**
@@ -37,7 +51,7 @@
  this.rowcount = 0;
  this.colcount = 0;
  this.subject_col = -1;
  this.subject_col = 0;
  this.modkey = 0;
  this.multiselect = false;
  this.multiexpand = false;
@@ -46,12 +60,14 @@
  this.column_movable = false;
  this.keyboard = false;
  this.toggleselect = false;
  this.aria_listbox = false;
  this.parent_focus = true;
  this.drag_active = false;
  this.col_drag_active = false;
  this.column_fixed = null;
  this.last_selected = 0;
  this.shift_start = 0;
  this.last_selected = null;
  this.shift_start = null;
  this.focused = false;
  this.drag_mouse_start = null;
  this.dblclick_time = 500; // default value on MS Windows is 500
@@ -61,6 +77,9 @@
  if (p && typeof p === 'object')
    for (var n in p)
      this[n] = p[n];
  // register this instance
  rcube_list_widget._instances.push(this);
};
@@ -80,6 +99,14 @@
    this.tbody = this.list;
  }
  if ($(this.list).attr('role') == 'listbox') {
    this.aria_listbox = true;
    if (this.multiselect)
      $(this.list).attr('aria-multiselectable', 'true');
  }
  var me = this;
  if (this.tbody) {
    this.rows = {};
    this.rowcount = 0;
@@ -87,15 +114,25 @@
    var r, len, rows = this.tbody.childNodes;
    for (r=0, len=rows.length; r<len; r++) {
      this.rowcount += this.init_row(rows[r]) ? 1 : 0;
      if (rows[r].nodeType == 1)
        this.rowcount += this.init_row(rows[r]) ? 1 : 0;
    }
    this.init_header();
    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'});
      // allow the table element to receive focus.
      $(this.list).attr('tabindex', '0')
        .on('focus', function(e){ me.focus(e); });
    }
  }
  if (this.parent_focus) {
    this.list.parentNode.onclick = function(e) { me.focus(); };
  }
  return this;
@@ -107,18 +144,24 @@
 */
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
    row.onmousedown = function(e){ return self.drag_row(e, this.uid); };
    row.onmouseup = function(e){ return self.click_row(e, this.uid); };
    $(row).data('uid', uid)
      // set eventhandlers to table row (only left-button-clicks in mouseup)
      .mousedown(function(e) { return self.drag_row(e, this.uid); })
      .mouseup(function(e) {
        if (e.which == 1 && !self.drag_active)
          return self.click_row(e, this.uid);
        else
          return true;
      });
    if (bw.touch) {
    if (bw.touch && row.addEventListener) {
      row.addEventListener('touchstart', function(e) {
        if (e.touches.length == 1) {
          self.touchmoved = false;
@@ -138,6 +181,15 @@
            e.preventDefault();
        }
      }, false);
    }
    // label the list row with the subject col as descriptive label
    if (this.aria_listbox) {
      var lbl_id = 'l:' + row.id;
      $(row)
        .attr('role', 'option')
        .attr('aria-labelledby', lbl_id)
        .find(this.col_tagname()).eq(this.subject_col).attr('id', lbl_id);
    }
    if (document.all)
@@ -161,7 +213,7 @@
    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('th,td').attr('style', '').find('a').attr('tabindex', '-1');  // remove fixed widths
    }
    else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
      this.init_fixed_header();
@@ -174,7 +226,7 @@
        if (this.column_fixed == r)
          continue;
        col = this.thead.rows[0].cells[r];
        col.onmousedown = function(e){ return p.drag_column(e, this); };
        col.onmousedown = function(e) { return p.drag_column(e, this); };
        this.colcount++;
      }
    }
@@ -188,17 +240,31 @@
  if (!this.fixed_header) {
    this.fixed_header = $('<table>')
      .attr('class', this.list.className + ' fixedcopy')
      .attr('role', 'presentation')
      .css({ position:'fixed' })
      .append(clone)
      .append('<tbody></tbody>');
    $(this.list).before(this.fixed_header);
    var me = this;
    $(window).resize(function(){ me.resize() });
    $(window).resize(function() { me.resize(); });
    $(window).scroll(function() {
      var w = $(window);
      me.fixed_header.css({
        marginLeft: -w.scrollLeft() + 'px',
        marginTop: -w.scrollTop() + 'px'
      });
    });
  }
  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();
@@ -212,15 +278,17 @@
    var column_widths = [];
    // get column widths from original thead
    $(this.tbody).parent().find('thead tr td').each(function(index) {
    $(this.tbody).parent().find('thead th,thead td').each(function(index) {
      column_widths[index] = $(this).width();
    });
    // apply fixed widths to fixed table header
    $(this.thead).parent().width($(this.tbody).parent().width());
    $(this.thead).find('tr td').each(function(index) {
      $(this).css('width', column_widths[index]);
    $(this.thead).find('th,td').each(function(index) {
      $(this).width(column_widths[index]);
    });
    $(window).scroll();
},
/**
@@ -240,6 +308,7 @@
  this.rows = {};
  this.rowcount = 0;
  this.last_selected = null;
  if (sel)
    this.clear_selection();
@@ -288,15 +357,19 @@
  if (row.nodeName === undefined) {
    // for performance reasons use DOM instead of jQuery here
    var domrow = document.createElement(this.row_tagname());
    if (row.id) domrow.id = row.id;
    if (row.uid) domrow.uid = row.uid;
    if (row.className) domrow.className = row.className;
    if (row.style) $.extend(domrow.style, row.style);
    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);
    }
@@ -324,18 +397,22 @@
  var row = this.rows[id];
  if (!row) return false;
  var domrow = row.obj;
  for (var domcell, col, i=0; cols && i < cols.length; i++) {
  var i, domrow = row.obj;
  for (i = 0; cols && i < cols.length; i++) {
    this.get_cell(domrow, i).html(cols[i]);
  }
  if (newid) {
    delete this.rows[id];
    domrow.uid = newid;
    domrow.id = 'rcmrow' + newid;
    this.init_row(domrow);
    if (select)
      this.selection[0] = newid;
    if (this.last_selected == id)
      this.last_selected = newid;
  }
},
@@ -345,39 +422,66 @@
 */
focus: function(e)
{
  var n, id;
  if (this.focused)
    return;
  this.focused = true;
  for (n in this.selection) {
    id = this.selection[n];
    if (this.rows[id] && this.rows[id].obj) {
      $(this.rows[id].obj).addClass('selected').removeClass('unfocused');
    }
  if (e)
    rcube_event.cancel(e);
  var focus_elem = null;
  if (this.last_selected && this.rows[this.last_selected]) {
    focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0');
  }
  // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
  // It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
  $(':focus:not(body)').blur();
  window.focus();
  if (focus_elem && focus_elem.length) {
    // We now fix this by explicitly assigning focus to a dedicated link element
    this.focus_noscroll(focus_elem);
  }
  else {
    // 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))
    rcube_event.cancel(e);
  $(this.list).addClass('focus').removeAttr('tabindex');
  // 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;
  for (n in this.selection) {
    id = this.selection[n];
    if (this.rows[id] && this.rows[id].obj) {
      $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused');
    }
  // avoid the table getting focus right again
  var me = this;
  setTimeout(function(){
    $(me.list).removeClass('focus').attr('tabindex', '0');
  }, 20);
  if (this.last_selected && this.rows[this.last_selected]) {
    $(this.rows[this.last_selected].obj)
      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
  }
},
/**
 * Focus the given element without scrolling the list container
 */
focus_noscroll: function(elem)
{
  var y = this.frame.scrollTop || this.frame.scrollY;
  elem.focus();
  this.frame.scrollTop = y;
},
@@ -468,6 +572,10 @@
 */
click_row: function(e, id)
{
  // sanity check
  if (!id || !this.rows[id])
    return false;
  // don't do anything (another action processed before)
  if (!this.is_event_target(e))
    return true;
@@ -497,6 +605,8 @@
  }
  this.rows[id].clicked = now;
  this.focus();
  return false;
},
@@ -589,7 +699,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 {
@@ -639,7 +749,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 
@@ -661,7 +771,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 });
        }
      }
@@ -683,7 +793,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 {
@@ -700,7 +810,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 });
        }
      }
@@ -714,13 +824,28 @@
},
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)
    return;
  if (!row.uid) {
    var uid = $(row).data('uid');
    if (uid)
      row.uid = uid;
    else if (String(row.id).match(this.id_regexp))
      row.uid = RegExp.$1;
  }
  return row.uid;
},
/**
 * get first/next/previous/last rows that are not hidden
@@ -756,11 +881,11 @@
get_first_row: function()
{
  if (this.rowcount) {
    var i, len, rows = this.tbody.childNodes;
    var i, 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;
    for (i=0; i<rows.length; i++)
      if (rows[i].id && (uid = this.get_row_uid(rows[i])))
        return uid;
  }
  return null;
@@ -769,11 +894,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;
@@ -801,9 +926,10 @@
 */
select_row: function(id, mod_key, with_mouse)
{
  var select_before = this.selection.join(',');
  var select_before = this.selection.join(','),
    in_selection_before = this.in_selection(id);
  if (!this.multiselect)
  if (!this.multiselect && with_mouse)
    mod_key = 0;
  if (!this.shift_start)
@@ -839,20 +965,26 @@
    this.multi_selecting = true;
  }
  // trigger event if selection changed
  if (this.selection.join(',') != select_before)
    this.triggerEvent('select');
  if (this.last_selected != 0 && this.rows[this.last_selected])
    $(this.rows[this.last_selected].obj).removeClass('focused');
  if (this.last_selected && this.rows[this.last_selected]) {
    $(this.rows[this.last_selected].obj).removeClass('focused')
      .find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
  }
  // unselect if toggleselect is active and the same row was clicked again
  if (this.toggleselect && this.last_selected == id) {
  if (this.toggleselect && in_selection_before && !mod_key) {
    this.clear_selection();
    id = null;
  }
  else
  // trigger event if selection changed
  else if (this.selection.join(',') != select_before) {
    this.triggerEvent('select');
  }
  if (this.rows[id]) {
    $(this.rows[id].obj).addClass('focused');
    // set cursor focus to link inside selected row
    if (this.focused)
      this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_col).attr('tabindex', '0'));
  }
  if (!this.selection.length)
    this.shift_start = null;
@@ -972,11 +1104,11 @@
/**
 * Check if given id is part of the current selection
 */
in_selection: function(id)
in_selection: function(id, index)
{
  for (var n in this.selection)
    if (this.selection[n] == id)
      return true;
      return index ? parseInt(n) : true;
  return false;
},
@@ -1000,7 +1132,7 @@
      this.highlight_row(n, true, true);
    }
    else {
      $(this.rows[n].obj).removeClass('selected').removeClass('unfocused');
      $(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');
    }
  }
@@ -1041,7 +1173,7 @@
/**
 * Unselect selected row(s)
 */
clear_selection: function(id)
clear_selection: function(id, no_event)
{
  var n, num_select = this.selection.length;
@@ -1057,14 +1189,16 @@
  else {
    for (n in this.selection)
      if (this.rows[this.selection[n]]) {
        $(this.rows[this.selection[n]].obj).removeClass('selected').removeClass('unfocused');
        $(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');
      }
    this.selection = [];
  }
  if (num_select && !this.selection.length)
  if (num_select && !this.selection.length && !no_event) {
    this.triggerEvent('select');
    this.last_selected = null;
  }
},
@@ -1116,25 +1250,26 @@
  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');
      $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
    }
  }
  else {
    if (!this.in_selection(id)) { // select row
    var pre, post, p = this.in_selection(id, true);
    if (p === false) { // select row
      this.selection.push(id);
      $(this.rows[id].obj).addClass('selected');
      $(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
      if (!norecur && !this.rows[id].expanded)
        this.highlight_children(id, true);
    }
    else { // unselect row
      var p = $.inArray(id, this.selection),
        a_pre = this.selection.slice(0, p),
        a_post = this.selection.slice(p+1, this.selection.length);
      pre = this.selection.slice(0, p);
      post = this.selection.slice(p+1, this.selection.length);
      this.selection = a_pre.concat(a_post);
      $(this.rows[id].obj).removeClass('selected').removeClass('unfocused');
      this.selection = pre.concat(post);
      $(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
      if (!norecur && !this.rows[id].expanded)
        this.highlight_children(id, false);
    }
@@ -1214,6 +1349,14 @@
      return rcube_event.cancel(e);
    case 9: // Tab
      this.blur();
      break;
    case 13: // Enter
      if (!this.selection.length)
        this.select_row(this.last_selected, mod_key, false);
    default:
      this.key_pressed = keyCode;
      this.modkey = mod_key;
@@ -1267,14 +1410,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;
@@ -1286,7 +1437,8 @@
 */
scrollto: function(id)
{
  var row = this.rows[id].obj;
  var row = this.rows[id] ? this.rows[id].obj : null;
  if (row && this.frame) {
    var scroll_to = Number(row.offsetTop),
      head_offset = 0;
@@ -1346,12 +1498,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);
@@ -1378,7 +1527,11 @@
      $('> ' + self.col_tagname(), self.rows[uid].obj).each(function(n, cell) {
        if (self.subject_col < 0 || (self.subject_col >= 0 && self.subject_col == n)) {
          var subject = $(cell).text();
          // remove elements marked with "skip-on-drag" class
          cell = $(cell).clone();
          $(cell).find('.skip-on-drag').remove();
          var subject = cell.text();
          if (subject) {
            // remove leading spaces
@@ -1535,32 +1688,33 @@
    this.col_draglayer = null;
  }
  if (this.col_drag_active)
    this.focus();
  this.col_drag_active = false;
  rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
  rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
  // remove temp divs
  this.del_dragfix();
  if (this.selected_column !== null && this.cols && this.cols.length) {
    var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
  if (this.col_drag_active) {
    this.col_drag_active = false;
    this.focus();
    this.triggerEvent('column_dragend', e);
    // find destination position
    for (i=0; i<this.cols.length; i++) {
      if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
        cpos += this.cols[i];
      else
        break;
    }
    if (this.selected_column !== null && this.cols && this.cols.length) {
      var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
    if (i != this.selected_column && i != this.selected_column+1) {
      this.column_replace(this.selected_column, i);
      // find destination position
      for (i=0; i<this.cols.length; i++) {
        if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
          cpos += this.cols[i];
        else
          break;
      }
      if (i != this.selected_column && i != this.selected_column+1) {
        this.column_replace(this.selected_column, i);
      }
    }
  }
  this.triggerEvent('column_dragend', e);
  return rcube_event.cancel(e);
},
@@ -1673,3 +1827,6 @@
rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
// static
rcube_list_widget._instances = [];