From 344943f6ce40bb40470dba4e2ba56b366d493617 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Fri, 01 Feb 2013 12:24:42 -0500
Subject: [PATCH] Move some list manipulation functionality to the new treelist widget

---
 program/js/treelist.js |  162 ++++++++++++++++++++++++++++++-
 program/js/app.js      |  107 ++++----------------
 2 files changed, 178 insertions(+), 91 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index dbb65ee..7c6dba8 100644
--- a/program/js/app.js
+++ b/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;
-
-    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 {
+      $(this.treelist.get_item(key)).children().first().html(prop.name);
+      this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
     }
-    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))) {
diff --git a/program/js/treelist.js b/program/js/treelist.js
index 47ac0c1..4beaada 100644
--- a/program/js/treelist.js
+++ b/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')) {

--
Gitblit v1.9.1