Thomas Bruederli
2013-02-01 344943f6ce40bb40470dba4e2ba56b366d493617
Move some list manipulation functionality to the new treelist widget
2 files modified
267 ■■■■■ changed files
program/js/app.js 105 ●●●● patch | view | raw | blame | history
program/js/treelist.js 162 ●●●●● patch | view | raw | blame | history
program/js/app.js
@@ -472,6 +472,7 @@
        });
        this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('select', function(node){ ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
      }
    }
@@ -4416,11 +4417,8 @@
  // callback from server upon group-delete command
  this.remove_group_item = function(prop)
  {
    var li, key = 'G'+prop.source+prop.id;
    if ((li = this.get_folder_li(key,'',true))) {
      this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
      li.parentNode.removeChild(li);
    var key = 'G'+prop.source+prop.id;
    if (this.treelist.remove(key)) {
      delete this.env.contactfolders[key];
      delete this.env.contactgroups[key];
    }
@@ -4524,14 +4522,12 @@
      link = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.id)
        .click(function() { return rcmail.command('listgroup', prop, this); })
        .html(prop.name),
      li = $('<li>').attr({id: 'rcmli'+this.html_identifier(key), 'class': 'contactgroup'})
        .append(link);
        .html(prop.name);
    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
    this.add_contact_group_row(prop, li);
    this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true);
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
  };
  // callback for renaming a contact group
@@ -4540,15 +4536,13 @@
    this.reset_add_input();
    var key = 'G'+prop.source+prop.id,
      li = this.get_folder_li(key,'',true),
      link;
      newnode = {};
    // group ID has changed, replace link node and identifiers
    if (li && prop.newid) {
    if (prop.newid) {
      var newkey = 'G'+prop.source+prop.newid,
        newprop = $.extend({}, prop);;
        newprop = $.extend({}, prop);
      li.id = 'rcmli' + this.html_identifier(newkey);
      this.env.contactfolders[newkey] = this.env.contactfolders[key];
      this.env.contactfolders[newkey].id = prop.newid;
      this.env.group = prop.newid;
@@ -4559,45 +4553,22 @@
      newprop.id = prop.newid;
      newprop.type = 'group';
      link = $('<a>').attr('href', '#')
      newnode.id = newkey;
      newnode.html = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.newid)
        .click(function() { return rcmail.command('listgroup', newprop, this); })
        .html(prop.name);
      $(li).children().replaceWith(link);
    }
    // update displayed group name
    else if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
      link.innerHTML = prop.name;
    else {
      $(this.treelist.get_item(key)).children().first().html(prop.name);
    this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
    this.add_contact_group_row(prop, $(li), true);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
  };
  // add contact group row to the list, with sorting
  this.add_contact_group_row = function(prop, li, reloc)
  {
    var row, name = prop.name.toUpperCase(),
      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) {
      row = li.clone(true);
      li.remove();
    }
    else
      row = li;
    $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    // update list node and re-sort it
    this.treelist.update(key, newnode, true);
    row.insertAfter(sibling);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
  };
  this.update_group_commands = function()
@@ -4829,45 +4800,14 @@
        .attr('rel', id)
        .click(function() { return rcmail.command('listsearch', id, this); })
        .html(name),
      li = $('<li>').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' })
        .append(link),
      prop = {name:name, id:id, li:li[0]};
      prop = { name:name, id:id };
    this.add_saved_search_row(prop, li);
    this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
    this.select_folder(key,'',true);
    this.enable_command('search-delete', true);
    this.env.search_id = id;
    this.triggerEvent('abook_search_insert', prop);
  };
  // add saved search row to the list, with sorting
  this.add_saved_search_row = function(prop, li, reloc)
  {
    var row, sibling, name = prop.name.toUpperCase();
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
      row = li.clone(true);
      li.remove();
    }
    else
      row = li;
    $('li[class~="contactsearch"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (!sibling)
        sibling = this.previousSibling;
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    if (sibling)
      row.insertAfter(sibling);
    else
      row.appendTo(this.gui_objects.folderlist);
  };
  // creates an input for saved search name
@@ -4888,10 +4828,8 @@
  this.remove_search_item = function(id)
  {
    var li, key = 'S'+id;
    if ((li = this.get_folder_li(key,'',true))) {
    if (this.treelist.remove(key)) {
      this.triggerEvent('search_delete', { id:id, li:li });
      li.parentNode.removeChild(li);
    }
    this.env.search_id = null;
@@ -5716,7 +5654,10 @@
  // mark a mailbox as selected and set environment variable
  this.select_folder = function(name, prefix, encode)
  {
    if (this.gui_objects.folderlist) {
    if (this.treelist) {
      this.treelist.select(name);
    }
    else if (this.gui_objects.folderlist) {
      var current_li, target_li;
      if ((current_li = $('li.selected', this.gui_objects.folderlist))) {
program/js/treelist.js
@@ -55,6 +55,11 @@
    this.drag_start = drag_start;
    this.drag_end = drag_end;
    this.intersects = intersects;
    this.update = update_node;
    this.insert = insert;
    this.remove = remove;
    this.get_item = get_item;
    this.get_selection = get_selection;
    /////// startup code (constructor)
@@ -68,8 +73,7 @@
    }
    // load data from DOM
    else {
        data = walk_list(container);
        // console.log(data);
        update_data();
    }
    // register click handlers on list
@@ -170,6 +174,131 @@
    }
    /**
     * Insert the given node
     */
    function insert(node, parent_id, sort)
    {
        var li, parent_li,
            parent_node = parent_id ? indexbyid[parent_id] : null;
        // insert as child of an existing node
        if (parent_node) {
            if (!parent_node.children)
                parent_node.children = [];
            parent_node.children.push(node);
            parent_li = id2dom(parent_id);
            // re-render the entire subtree
            if (parent_node.children.length == 1) {
                render_node(parent_node, parent_li.parent(), parent_li);
                li = id2dom(node.id);
            }
            else {
                // append new node to parent's child list
                li = render_node(node, parent_li.children('ul').first());
            }
        }
        // insert at top level
        else {
            data.push(node);
            li = render_node(node, container);
        }
        indexbyid[node.id] = node;
        if (sort) {
            resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : '');
        }
    }
    /**
     * Update properties of an existing node
     */
    function update_node(id, updates, sort)
    {
        var li, node = indexbyid[id];
        if (node) {
            li = id2dom(id);
            if (updates.id || updates.html || updates.children || updates.classes) {
                $.extend(node, updates);
                render_node(node, li.parent(), li);
            }
            if (node.id != id) {
                delete indexbyid[id];
                indexbyid[node.id] = node;
            }
            if (sort) {
                resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : '');
            }
        }
    }
    /**
     * Helper method to sort the list of the given item
     */
    function resort_node(li, filter)
    {
        var first, sibling,
            myid = li.get(0).id,
            sortname = li.children().first().text().toUpperCase();
        li.parent().children('li' + filter).each(function(i, elem) {
            if (i == 0)
                first = elem;
            if (elem.id == myid) {
                // skip
            }
            else if (elem.id != myid && sortname >= $(elem).children().first().text().toUpperCase()) {
                sibling = elem;
            }
            else {
                return false;
            }
        });
        if (sibling) {
            li.insertAfter(sibling);
        }
        else {
            li.insertBefore(first);
        }
        // reload data from dom
        update_data();
    }
    /**
     * Remove the item with the given ID
     */
    function remove(id)
    {
        var node, li;
        if (node = indexbyid[id]) {
            li = id2dom(id);
            li.remove();
            node.deleted = true;
            delete indexbyid[id];
            return true;
        }
        return false;
    }
    /**
     * (Re-)read tree data from DOM
     */
    function update_data()
    {
        data = walk_list(container);
    }
    /**
     * Apply the 'collapsed' status of the data node to the corresponding DOM element(s)
     */
    function update_dom(node)
@@ -202,12 +331,26 @@
    /**
     * Render a specific node into the DOM list
     */
    function render_node(node, parent)
    function render_node(node, parent, replace)
    {
        var li = $('<li>' + node.html + '</li>')
            .attr('id', p.id_prefix + node.id)
            .addClass((node.classes || []).join(' '))
            .appendTo(parent);
        if (node.deleted)
            return;
        var li = $('<li>')
            .attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id))
            .addClass((node.classes || []).join(' '));
        if (replace)
            replace.replaceWith(li);
        else
            li.appendTo(parent);
        if (typeof node.html == 'string') {
            li.html(node.html);
        }
        else if (typeof node.html == 'object') {
            li.append(node.html);
        }
        if (node.virtual)
            li.addClass('virtual');
@@ -217,7 +360,7 @@
        // 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);
            var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass);
            if (node.collapsed)
                ul.hide();
@@ -225,6 +368,8 @@
                render_node(node.children[i], ul);
            }
        }
        return li;
    }
    /**
@@ -245,6 +390,7 @@
            }
            if (node.children.length) {
                node.childlistclass = li.children('ul').attr('class');
                node.collapsed = li.children('ul').css('display') == 'none';
            }
            if (li.hasClass('selected')) {