Thomas Bruederli
2013-01-31 3c309af0663f1673f921682c6ae3f673426e1397
- Refactored the hierarchical mailboxlist control into a separate widget class
- Build address book directories list as hierarchical list
- Make address book groups collapsible using the new new treelist widget
- Use encoded identifiers for address book directory list items
1 files added
7 files modified
738 ■■■■ changed files
program/include/rcmail.php 14 ●●●● patch | view | raw | blame | history
program/js/app.js 206 ●●●●● patch | view | raw | blame | history
program/js/treelist.js 425 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/func.inc 28 ●●●● patch | view | raw | blame | history
skins/classic/addressbook.css 39 ●●●●● patch | view | raw | blame | history
skins/classic/templates/addressbook.html 3 ●●●● patch | view | raw | blame | history
skins/larry/addressbook.css 21 ●●●●● patch | view | raw | blame | history
skins/larry/templates/addressbook.html 2 ●●● patch | view | raw | blame | history
program/include/rcmail.php
@@ -1406,6 +1406,7 @@
            $js_mailboxlist = array();
            $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
            $rcmail->output->include_script('treelist.js');
            $rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
            $rcmail->output->set_env('mailboxes', $js_mailboxlist);
            $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
@@ -1584,14 +1585,13 @@
                'id' => "rcmli".$folder_id,
                'class' => join(' ', $classes),
                'noclose' => true),
                html::a($link_attrib, $html_name) .
                (!empty($folder['folders']) ? html::div(array(
                    'class' => ($is_collapsed ? 'collapsed' : 'expanded'),
                    'style' => "position:absolute",
                    'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail_output::JS_OBJECT_NAME, $js_name)
                ), ' ') : ''));
                html::a($link_attrib, $html_name));
            $jslist[$folder_id] = array(
            if (!empty($folder['folders'])) {
                $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' ');
            }
            $jslist[$folder['id']] = array(
                'id'      => $folder['id'],
                'name'    => $foldername,
                'virtual' => $folder['virtual']
program/js/app.js
@@ -4,7 +4,7 @@
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
 | Copyright (C) 2011-2012, Kolab Systems AG                             |
 | Copyright (C) 2011-2013, Kolab Systems AG                             |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
@@ -459,8 +459,21 @@
      this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
    // map implicit containers
    if (this.gui_objects.folderlist)
    if (this.gui_objects.folderlist) {
      this.gui_containers.foldertray = $(this.gui_objects.folderlist);
      // init treelist widget
      if (window.rcube_treelist_widget) {
        this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
          id_prefix: 'rcmli',
          id_encode: this.html_identifier_encode,
          id_decode: this.html_identifier_decode,
          check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) }
        });
        this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
      }
    }
    // activate html5 file drop feature (if browser supports it and if configured)
    if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
@@ -1263,11 +1276,12 @@
  this.html_identifier = function(str, encode)
  {
    str = String(str);
    if (encode)
      return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
    else
      return str.replace(this.identifier_expr, '_');
    return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
  };
  this.html_identifier_encode = function(str)
  {
    return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
  };
  this.html_identifier_decode = function(str)
@@ -1320,29 +1334,9 @@
    if (this.preview_read_timer)
      clearTimeout(this.preview_read_timer);
    // save folderlist and folders location/sizes for droptarget calculation in drag_move()
    if (this.gui_objects.folderlist && model) {
      this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset;
      this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop;
      var k, li, height,
        list = $(this.gui_objects.folderlist);
        pos = list.offset();
      this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() };
      this.env.folder_coords = [];
      for (k in model) {
        if (li = this.get_folder_li(k)) {
          // only visible folders
          if (height = li.firstChild.offsetHeight) {
            pos = $(li.firstChild).offset();
            this.env.folder_coords[k] = { x1:pos.left, y1:pos.top,
              x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 };
          }
        }
      }
    }
    // prepare treelist widget for dragging interactions
    if (this.treelist)
      this.treelist.drag_start();
  };
  this.drag_end = function(e)
@@ -1350,87 +1344,28 @@
    this.drag_active = false;
    this.env.last_folder_target = null;
    if (this.folder_auto_timer) {
      clearTimeout(this.folder_auto_timer);
      this.folder_auto_timer = null;
      this.folder_auto_expand = null;
    }
    // over the folders
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      for (var k in this.env.folder_coords) {
        if (this.env.folder_coords[k].on)
          $(this.get_folder_li(k)).removeClass('droptarget');
      }
    }
    if (this.treelist)
      this.treelist.drag_end();
  };
  this.drag_move = function(e)
  {
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      var k, li, div, check, oldclass,
    if (this.gui_objects.folderlist) {
      var drag_target, oldclass,
        layerclass = 'draglayernormal',
        mouse = rcube_event.get_mouse_pos(e),
        pos = this.env.folderlist_coords,
        // offsets to compensate for scrolling while dragging a message
        boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop,
        moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop;
        mouse = rcube_event.get_mouse_pos(e);
      if (this.contact_list && this.contact_list.draglayer)
        oldclass = this.contact_list.draglayer.attr('class');
      mouse.y += -moffset-boffset;
      // if mouse pointer is outside of folderlist
      if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) {
        if (this.env.last_folder_target) {
          $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
          this.env.folder_coords[this.env.last_folder_target].on = 0;
          this.env.last_folder_target = null;
        }
        if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
          this.contact_list.draglayer.attr('class', layerclass);
        return;
      // mouse intersects a valid drop target on the treelist
      if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
        this.env.last_folder_target = drag_target;
        layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
      }
      // over the folders
      for (k in this.env.folder_coords) {
        pos = this.env.folder_coords[k];
        if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) {
          if (check = this.check_droptarget(k)) {
            li = this.get_folder_li(k);
            div = $(li.getElementsByTagName('div')[0]);
            // if the folder is collapsed, expand it after 1sec and restart the drag & drop process.
            if (div.hasClass('collapsed')) {
              if (this.folder_auto_timer)
                clearTimeout(this.folder_auto_timer);
              this.folder_auto_expand = this.env.mailboxes[k].id;
              this.folder_auto_timer = setTimeout(function() {
                rcmail.command('collapse-folder', rcmail.folder_auto_expand);
                rcmail.drag_start(null);
              }, 1000);
            }
            else if (this.folder_auto_timer) {
              clearTimeout(this.folder_auto_timer);
              this.folder_auto_timer = null;
              this.folder_auto_expand = null;
            }
            $(li).addClass('droptarget');
            this.env.folder_coords[k].on = 1;
            this.env.last_folder_target = k;
            layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal');
          }
          // Clear target, otherwise drag end will trigger move into last valid droptarget
          else
            this.env.last_folder_target = null;
        }
        else if (pos.on) {
          $(this.get_folder_li(k)).removeClass('droptarget');
          this.env.folder_coords[k].on = 0;
        }
      else {
        // Clear target, otherwise drag end will trigger move into last valid droptarget
        this.env.last_folder_target = null;
      }
      if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
@@ -1440,40 +1375,33 @@
  this.collapse_folder = function(name)
  {
    var li = this.get_folder_li(name, '', true),
      div = $('div:first', li),
      ul = $('ul:first', li);
    if (this.treelist)
      this.treelist.toggle(name);
  };
    if (div.hasClass('collapsed')) {
      ul.show();
      div.removeClass('collapsed').addClass('expanded');
      var reg = new RegExp('&'+urlencode(name)+'&');
      this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, '');
    }
    else if (div.hasClass('expanded')) {
      ul.hide();
      div.removeClass('expanded').addClass('collapsed');
      this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&';
  this.folder_collapsed = function(node)
  {
    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
    if (node.collapsed) {
      this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
      // select the folder if one of its childs is currently selected
      // don't select if it's virtual (#1488346)
      if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual'))
      if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual)
        this.command('list', name);
    }
    else
      return;
    // Work around a bug in IE6 and IE7, see #1485309
    if (bw.ie6 || bw.ie7) {
      var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null;
      if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') {
        li.style.display = 'none';
        li.style.display = '';
      }
    else {
      var reg = new RegExp('&'+urlencode(node.id)+'&');
      this.env[prefname] = this.env[prefname].replace(reg, '');
    }
    this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
    this.set_unread_count_display(name, false);
    if (!this.drag_active) {
      this.command('save-pref', { name: prefname, value: this.env[prefname] });
      if (this.env.unread_counts)
        this.set_unread_count_display(node.id, false);
    }
  };
  this.doc_mouse_up = function(e)
@@ -1498,9 +1426,9 @@
    if (this.drag_active && model && this.env.last_folder_target) {
      var target = model[this.env.last_folder_target];
      $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
      this.env.last_folder_target = null;
      list.draglayer.hide();
      this.drag_end(e);
      if (!this.drag_menu(e, target))
        this.command('moveto', target);
@@ -4164,7 +4092,7 @@
    else if (!this.env.search_request)
      folder = group ? 'G'+src+group : src;
    this.select_folder(folder);
    this.select_folder(folder, '', true);
    this.env.source = src;
    this.env.group = group;
@@ -4468,7 +4396,7 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.env.group_renaming = true;
      var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG');
      var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
      if (li && (link = li.firstChild)) {
        $(link).hide().before(this.name_input);
      }
@@ -4489,7 +4417,7 @@
  this.remove_group_item = function(prop)
  {
    var li, key = 'G'+prop.source+prop.id;
    if ((li = this.get_folder_li(key))) {
    if ((li = this.get_folder_li(key,'',true))) {
      this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
      li.parentNode.removeChild(li);
@@ -4511,7 +4439,7 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.name_input_li = $('<li>').addClass(type).append(this.name_input);
      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source);
      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : $('ul.groups li:last', this.get_folder_li(this.env.source,'',true));
      this.name_input_li.insertAfter(li);
    }
@@ -4612,7 +4540,7 @@
    this.reset_add_input();
    var key = 'G'+prop.source+prop.id,
      li = this.get_folder_li(key),
      li = this.get_folder_li(key,'',true),
      link;
    // group ID has changed, replace link node and identifiers
@@ -4651,8 +4579,8 @@
  this.add_contact_group_row = function(prop, li, reloc)
  {
    var row, name = prop.name.toUpperCase(),
      sibling = this.get_folder_li(prop.source),
      prefix = 'rcmliG' + this.html_identifier(prop.source);
      sibling = this.get_folder_li(prop.source,'',true),
      prefix = 'rcmli' + this.html_identifier('G'+prop.source, true);
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
@@ -4901,12 +4829,12 @@
        .attr('rel', id)
        .click(function() { return rcmail.command('listsearch', id, this); })
        .html(name),
      li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'})
      li = $('<li>').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' })
        .append(link),
      prop = {name:name, id:id, li:li[0]};
    this.add_saved_search_row(prop, li);
    this.select_folder('S'+id);
    this.select_folder(key,'',true);
    this.enable_command('search-delete', true);
    this.env.search_id = id;
@@ -4960,7 +4888,7 @@
  this.remove_search_item = function(id)
  {
    var li, key = 'S'+id;
    if ((li = this.get_folder_li(key))) {
    if ((li = this.get_folder_li(key,'',true))) {
      this.triggerEvent('search_delete', { id:id, li:li });
      li.parentNode.removeChild(li);
@@ -4982,7 +4910,7 @@
    }
    this.reset_qsearch();
    this.select_folder('S'+id);
    this.select_folder('S'+id, '', true);
    // reset vars
    this.env.current_page = 1;
program/js/treelist.js
New file
@@ -0,0 +1,425 @@
/*
 +-----------------------------------------------------------------------+
 | Roundcube Treelist widget                                             |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 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>                       |
 +-----------------------------------------------------------------------+
 | Requires: common.js                                                   |
 +-----------------------------------------------------------------------+
*/
/**
 * Roundcube Treelist widget class
 * @contructor
 */
function rcube_treelist_widget(node, p)
{
    // apply some defaults to p
    p = $.extend({
        id_prefix: '',
        autoexpand: 1000,
        selectable: false,
        check_droptarget: function(node){ return !node.virtual }
    }, p || {});
    var container = $(node);
    var data = p.data || [];
    var indexbyid = {};
    var selection = null;
    var drag_active = false;
    var box_coords = {};
    var item_coords = [];
    var autoexpand_timer;
    var autoexpand_item;
    var body_scroll_top = 0;
    var list_scroll_top = 0;
    var me = this;
    /////// export public members and methods
    this.container = container;
    this.expand = expand;
    this.collapse = collapse;
    this.select = select;
    this.render = render;
    this.drag_start = drag_start;
    this.drag_end = drag_end;
    this.intersects = intersects;
    /////// startup code (constructor)
    // abort if node not found
    if (!container.length)
        return;
    if (p.data) {
        index_data({ children:data });
    }
    // load data from DOM
    else {
        data = walk_list(container);
        // console.log(data);
    }
    // register click handlers on list
    container.on('click', 'div.treetoggle', function(e){
        toggle(dom2id($(this).parent()));
    });
    container.on('click', 'li', function(e){
        var node = p.selectable ? indexbyid[dom2id($(this))] : null;
        if (node && !node.virtual) {
            select(node.id);
            e.stopPropagation();
        }
    });
    /////// private methods
    /**
     * Collaps a the node with the given ID
     */
    function collapse(id, recursive, set)
    {
        var node;
        if (node = indexbyid[id]) {
            node.collapsed = typeof set == 'undefined' || set;
            update_dom(node);
            // Work around a bug in IE6 and IE7, see #1485309
            if (window.bw && (bw.ie6 || bw.ie7) && node.collapsed) {
                id2dom(node.id).next().children('ul:visible').hide().show();
            }
            if (recursive && node.children) {
                for (var i=0; i < node.children.length; i++) {
                    collapse(node.children[i].id, recursive, set);
                }
            }
            me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node);
        }
    }
    /**
     * Expand a the node with the given ID
     */
    function expand(id, recursive)
    {
        collapse(id, recursive, false);
    }
    /**
     * Toggle collapsed state of a list node
     */
    function toggle(id, recursive)
    {
        var node;
        if (node = indexbyid[id]) {
            collapse(id, recursive, !node.collapsed);
        }
    }
    /**
     * Select a tree node by it's ID
     */
    function select(id)
    {
        if (selection) {
            id2dom(selection).removeClass('selected');
            selection = null;
        }
        var li = id2dom(id);
        if (li.length) {
            li.addClass('selected');
            selection = id;
            // TODO: expand all parent nodes if collapsed
            scroll_to_node(li);
        }
        me.triggerEvent('select', indexbyid[id]);
    }
    /**
     * Getter for the currently selected node ID
     */
    function get_selection()
    {
        return selection;
    }
    /**
     * Return the DOM element of the list item with the given ID
     */
    function get_item(id)
    {
        return id2dom(id).get(0);
    }
    /**
     * Apply the 'collapsed' status of the data node to the corresponding DOM element(s)
     */
    function update_dom(node)
    {
        var li = id2dom(node.id);
        li.children('ul').first()[(node.collapsed ? 'hide' : 'show')]();
        li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded');
        me.triggerEvent('toggle', node);
    }
    /**
     * Render the tree list from the internal data structure
     */
    function render()
    {
        if (me.triggerEvent('renderBefore', data) === false)
            return;
        // remove all child nodes
        container.html('');
        // render child nodes
        for (var i=0; i < data.length; i++) {
            render_node(data[i], container);
        }
        me.triggerEvent('renderAfter', container);
    }
    /**
     * Render a specific node into the DOM list
     */
    function render_node(node, parent)
    {
        var li = $('<li>' + node.html + '</li>')
            .attr('id', p.id_prefix + node.id)
            .addClass((node.classes || []).join(' '))
            .appendTo(parent);
        if (node.virtual)
            li.addClass('virtual');
        if (node.id == selection)
            li.addClass('selected');
        // add child list and toggle icon
        if (node.children && node.children.length) {
            $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '">&nbsp;</div>').appendTo(li);
            var ul = $('<ul>').appendTo(li);
            if (node.collapsed)
                ul.hide();
            for (var i=0; i < node.children.length; i++) {
                render_node(node.children[i], ul);
            }
        }
    }
    /**
     * Recursively walk the DOM tree and build an internal data structure
     * representing the skeleton of this tree list.
     */
    function walk_list(ul)
    {
        var result = [];
        ul.children('li').each(function(i,e){
            var li = $(e);
            var node = {
                id: dom2id(li),
                classes: li.attr('class').split(' '),
                virtual: li.hasClass('virtual'),
                html: li.children().first().get(0).outerHTML,
                children: walk_list(li.children('ul'))
            }
            if (node.children.length) {
                node.collapsed = li.children('ul').css('display') == 'none';
            }
            if (li.hasClass('selected')) {
                selection = node.id;
            }
            result.push(node);
            indexbyid[node.id] = node;
        })
        return result;
    }
    /**
     * Recursively walk the data tree and index nodes by their ID
     */
    function index_data(node)
    {
        if (node.id) {
            indexbyid[node.id] = node;
        }
        for (var c=0; node.children && c < node.children.length; c++) {
            index_data(node.children[c]);
        }
    }
    /**
     * Get the (stripped) node ID from the given DOM element
     */
    function dom2id(li)
    {
        var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), '');
        return p.id_decode ? p.id_decode(domid) : domid;
    }
    /**
     * Get the <li> element for the given node ID
     */
    function id2dom(id)
    {
        var domid = p.id_encode ? p.id_encode(id) : id;
        return $('#' + p.id_prefix + domid);
    }
    /**
     * Scroll the parent container to make the given list item visible
     */
    function scroll_to_node(li)
    {
        var scroller = container.parent();
        scroller.scrollTop(li.offset().top - scroller.offset().top + scroller.scrollTop());
    }
    ///// drag & drop support
    /**
     * When dragging starts, compute absolute bounding boxes of the list and it's items
     * for faster comparisons while mouse is moving
     */
    function drag_start()
    {
        var li, item, height,
            pos = container.offset();
        body_scroll_top = bw.ie ? 0 : window.pageYOffset;
        list_scroll_top = container.parent().scrollTop();
        drag_active = true;
        box_coords = {
            x1: pos.left,
            y1: pos.top,
            x2: pos.left + container.width(),
            y2: pos.top + container.height()
        };
        item_coords = [];
        for (var id in indexbyid) {
            li = id2dom(id);
            item = li.children().first().get(0);
            if (height = item.offsetHeight) {
                pos = $(item).offset();
                item_coords[id] = {
                    x1: pos.left,
                    y1: pos.top,
                    x2: pos.left + item.offsetWidth,
                    y2: pos.top + height,
                    on: id == autoexpand_item
                };
            }
        }
    }
    /**
     * Signal that dragging has stopped
     */
    function drag_end()
    {
        drag_active = false;
        if (autoexpand_timer) {
            clearTimeout(autoexpand_timer);
            autoexpand_timer = null;
            autoexpand_item = null;
        }
        $('li.droptarget', container).removeClass('droptarget');
    }
    /**
     * Determine if the given mouse coords intersect the list and one if its items
     */
    function intersects(mouse, highlight)
    {
        // offsets to compensate for scrolling while dragging a message
        var boffset = bw.ie ? -document.documentElement.scrollTop : body_scroll_top,
            moffset = list_scroll_top - container.parent().scrollTop(),
            result = null;
        mouse.top = mouse.y + -moffset - boffset;
        // no intersection with list bounding box
        if (mouse.x < box_coords.x1 || mouse.x >= box_coords.x2 || mouse.top < box_coords.y1 || mouse.top >= box_coords.y2) {
          // TODO: optimize performance for this operation
          $('li.droptarget', container).removeClass('droptarget');
            return result;
        }
        // check intersection with visible list items
        var pos, node;
        for (var id in item_coords) {
            pos = item_coords[id];
            if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.top >= pos.y1 && mouse.top < pos.y2) {
                node = indexbyid[id];
                // if the folder is collapsed, expand it after the configured time
                if (node.children && node.children.length && node.collapsed && p.autoexpand && autoexpand_item != id) {
                    if (autoexpand_timer)
                        clearTimeout(autoexpand_timer);
                    autoexpand_item = id;
                    autoexpand_timer = setTimeout(function() {
                        expand(autoexpand_item);
                        drag_start();  // re-calculate item coords
                        autoexpand_item = null;
                    }, p.autoexpand);
                }
                else if (autoexpand_timer && autoexpand_item != id) {
                    clearTimeout(autoexpand_timer);
                    autoexpand_item = null;
                    autoexpand_timer = null;
                }
                // check if this item is accepted as drop target
                if (p.check_droptarget(node)) {
                    if (highlight) {
                        id2dom(id).addClass('droptarget');
                        pos.on = true;
                    }
                    result = id;
                }
                else {
                    result = null;
                }
            }
            else if (pos.on) {
                id2dom(id).removeClass('droptarget');
                pos.on = false;
            }
        }
        return result;
    }
}
// use event processing functions from Roundcube's rcube_event_engine
rcube_treelist_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_treelist_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_treelist_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
program/steps/addressbook/func.inc
@@ -187,7 +187,7 @@
    $jsdata = array();
    $line_templ = html::tag('li', array(
        'id' => 'rcmli%s', 'class' => '%s'),
        'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true),
        html::a(array('href' => '%s',
            'rel' => '%s',
            'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s'));
@@ -213,7 +213,7 @@
        $name = !empty($source['name']) ? $source['name'] : $id;
        $out .= sprintf($line_templ,
            html_identifier($id),
            rcube_utils::html_identifier($id, true),
            $class_name,
            Q(rcmail_url(null, array('_source' => $id))),
            $source['id'],
@@ -224,10 +224,11 @@
            $groupdata = rcmail_contact_groups($groupdata);
        $jsdata = $groupdata['jsdata'];
        $out = $groupdata['out'];
        $out .= '</li>';
    }
    $line_templ = html::tag('li', array(
        'id' => 'rcmliS%s', 'class' => '%s'),
        'id' => 'rcmli%s', 'class' => '%s'),
        html::a(array('href' => '#', 'rel' => 'S%s',
            'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s'));
@@ -245,14 +246,17 @@
            $class_name .= ' ' . $source['class_name'];
        $out .= sprintf($line_templ,
            html_identifier($id),
            rcube_utils::html_identifier('S'.$id, true),
            $class_name,
            $id,
            $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id)));
    }
    $OUTPUT->set_env('contactgroups', $jsdata);
    $OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks',''));
    $OUTPUT->add_gui_object('folderlist', $attrib['id']);
    $OUTPUT->include_script('treelist.js');
    // add some labels to client
    $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember');
@@ -265,18 +269,24 @@
    global $RCMAIL;
    $groups = $RCMAIL->get_address_book($args['source'])->list_groups();
    $js_id = $RCMAIL->JQ($args['source']);
    if (!empty($groups)) {
        $line_templ = html::tag('li', array(
            'id' => 'rcmliG%s', 'class' => 'contactgroup'),
            'id' => 'rcmli%s', 'class' => 'contactgroup'),
            html::a(array('href' => '#',
                'rel' => '%s:%s',
                'onclick' => "return ".JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s'));
        // append collapse/expand toggle and open a new <ul>
        $is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false;
        $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
        $jsdata = array();
        $groups_html = '';
        foreach ($groups as $group) {
            $args['out'] .= sprintf($line_templ,
                html_identifier($args['source'] . $group['ID']),
            $groups_html .= sprintf($line_templ,
                rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true),
                $args['source'], $group['ID'],
                $args['source'], $group['ID'], Q($group['name'])
            );
@@ -286,6 +296,10 @@
        }
    }
    $args['out'] .= html::tag('ul',
      array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)),
      $groups_html);
    return $args;
}
skins/classic/addressbook.css
@@ -118,7 +118,7 @@
#directorylistbox input
{
  margin: 0px;
  margin: 0 0 0 20px;
  font-size: 11px;
  width: 90%;
}
@@ -144,7 +144,8 @@
  width: 280px;
}
#directorylist
#directorylist,
#directorylist li ul
{
  list-style: none;
  margin: 0;
@@ -152,11 +153,15 @@
  background-color: #FFFFFF;
}
#directorylist li ul
{
  border-top: 1px solid #EBEBEB;
}
#directorylist li
{
  display: block;
  font-size: 11px;
  background: url(images/icons/folders.png) 5px -108px no-repeat;
  border-bottom: 1px solid #EBEBEB;
  white-space: nowrap;
}
@@ -168,31 +173,37 @@
  padding-left: 25px;
  padding-top: 2px;
  padding-bottom: 2px;
  height: 16px;
  text-decoration: none;
  white-space: nowrap;
  background: url(images/icons/folders.png) 5px -108px no-repeat;
}
#directorylist li.contactgroup
#directorylist li ul li a
{
  padding-left: 15px;
  background-position: 20px -143px;
  padding-left: 45px;
}
#directorylist li.contactsearch
#directorylist li ul li:last-child
{
  border-bottom: 0;
}
#directorylist li.contactgroup a
{
  background-position: 22px -143px;
}
#directorylist li.contactsearch a
{
  background-position: 6px -162px;
}
#directorylist li.selected
{
  background-color: #929292;
  border-bottom: 1px solid #898989;
}
#directorylist li.selected a
#directorylist li.selected > a
{
  color: #FFF;
  font-weight: bold;
  background-color: #929292;
}
#directorylist li.droptarget
skins/classic/templates/addressbook.html
@@ -63,8 +63,7 @@
<div id="directorylistbox">
<div id="directorylist-title" class="boxtitle"><roundcube:label name="groups" /></div>
<div id="directorylist-content" class="boxlistcontent">
  <roundcube:object name="directorylist" id="directorylist" />
  <roundcube:object name="groupslist" id="contactgroupslist" />
  <roundcube:object name="directorylist" id="directorylist" class="treelist" />
</div>
<div id="directorylist-footer" class="boxfooter">
  <roundcube:button command="group-create" type="link" title="newcontactgroup" class="buttonPas addgroup" classAct="button addgroup" content=" " />
skins/larry/addressbook.css
@@ -83,8 +83,21 @@
    background-position: 6px -766px;
}
#directorylist li.addressbook.selected a {
#directorylist li.addressbook.selected > a {
    background-position: 6px -791px;
}
#directorylist li.addressbook ul li:last-child {
    border-bottom: 0;
}
#directorylist li.addressbook ul.groups {
    margin: 0;
    padding: 0;
}
#directorylist li.addressbook ul.groups li {
    width: 100%;
}
#directorylist li.contactgroup a {
@@ -112,6 +125,12 @@
    margin-left: 8px;
}
#directorylist li.addressbook div.collapsed,
#directorylist li.addressbook div.expanded {
    top: 15px;
    left: 20px;
}
#contacts-table .contact td.name {
    background-position: 6px -1603px;
}
skins/larry/templates/addressbook.html
@@ -30,7 +30,7 @@
<div id="directorylistbox" class="uibox listbox">
<h2 id="directorylist-header" class="boxtitle"><roundcube:label name="groups" /></h2>
<div id="directorylist-content" class="scroller withfooter">
    <roundcube:object name="directorylist" id="directorylist" class="listing" />
    <roundcube:object name="directorylist" id="directorylist" class="treelist listing" />
</div>
<div id="directorylist-footer" class="boxfooter">
    <roundcube:button command="group-create" type="link" title="newcontactgroup" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="groupoptions" id="groupoptionslink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('groupoptions');return false" innerClass="inner" content="&#9881;" />