// Enable expanding/folding folders in TracBrowser

(function($){
  var FOLDERID_COUNTER = 0;
  var SUBFOLDER_INDENT = 20;
  
  // enableExpandDir adds the capability to folder rows to be expanded and folded
  // It also teach the rows about their ancestors. It expects:
  //  - `parent_tr`, the logical parent row (`null` if there's no ancestor)
  //  - a `rows` jQuery object matching the newly created entry rows
  //  - `qargs`, additional parameters to send to the server when expanding
  
  window.enableExpandDir = function(parent_tr, rows, qargs) {
    // the ancestors folder ids are present in the parent_tr class attribute
    var ancestor_folderids = [];
    if (parent_tr)
      ancestor_folderids = $.grep(parent_tr.attr("class").split(" "), 
                                  function(c) { return c.match(/^f\d+$/)});
    rows.each(function () {
      var a = $(this).find("a.dir");
  
      if (a.length) { // then the entry is a folder
        // create new folder id
        var folderid = "f" + FOLDERID_COUNTER++;
        this.id = folderid;
        $(this).addClass(folderid);
  
        // add the expander icon
        a.wrap('<div></div>');
        var expander = a.before('<span class="expander">&nbsp;</span>').prev();
        expander.attr("title", "Expand sub-directory in place")
          .click(function() { toggleDir($(this), qargs); });
      }
  
      // tie that row to ancestor folders
      if (parent_tr)
        $(this).addClass(ancestor_folderids.join(" "));
    });
  }
  
  // handler for click event on the expander icons
  window.toggleDir = function(expander, qargs) {
    var tr = expander.parents("tr:first");
    var folderid = tr.get(0).id;
  
    if ( tr.filter(".expanded").length ) { // then *fold*
      tr.removeClass("expanded").addClass("collapsed");
      tr.siblings("tr."+folderid).hide();
      expander.attr("title", "Re-expand directory");
      return;
    }
  
    if ( tr.filter(".collapsed").length ) { // then *expand*
      tr.removeClass("collapsed").addClass("expanded");
      tr.siblings("tr."+folderid).show();
      // Note that the above will show all the already fetched subtree,
      // so we have to fold again the folders which were already collapsed.
      tr.siblings("tr.collapsed").each(function() {
        tr.siblings("tr."+this.id).not(this).hide();
      });
    } else {                                // then *fetch*
      var td = expander.parents("td");
      var td_class = td.attr("class");
      var a = expander.next("a");
      var depth = 
        parseFloat(td.css("padding-left").replace(/^(\d*\.\d*).*$/, "$1")) + 
        SUBFOLDER_INDENT;
  
      tr.addClass("expanded");
      // insert "Loading ..." row
      tr.after('<tr><td><span class="loading"></span></td></tr>');
      var loading_row = tr.next();
      loading_row.children("td").addClass(td_class)
        .attr("colspan", tr.children("td").length)
        .css("padding-left", depth);
      loading_row.find("span.loading").text("Loading " + a.text() + "...");
  
      // XHR for getting the rows corresponding to the folder entries
      $.ajax({
        type: "GET",
        url: a.attr("href"),
        data: qargs,
        dataType: "html",
        success: function(data) {
          // Safari 3.1.1 has some trouble reconstructing HTML snippets
          // bigger than 50k - splitting in rows before building DOM nodes
          var rows = data.replace(/^<!DOCTYPE[^>]+>/, "").split("</tr>");
          if (rows.length) {
            // insert entry rows 
            $(rows).each(function() {
              row = $(this+"</tr>");
              row.children("td."+td_class).css("padding-left", depth);
              // make all entry rows collapsible but only subdir rows expandable
              enableExpandDir(tr, row, qargs); 
              loading_row.before(row);
            });
            // remove "Loading ..." row
            loading_row.remove();
          } else {
            loading_row.find("span.loading").text("").append("<i>(empty)</i>")
              .removeClass("loading");
            // make the (empty) row collapsible
            enableExpandDir(tr, loading_row, qargs); 
          }
        },
        error: function(req, err, exc) {
          loading_row.find("span.loading").text("").append("<i>(error)</i>")
            .removeClass("loading");
          enableExpandDir(tr, loading_row, qargs);
        }
      });
    }
    expander.attr("title", "Fold directory");
  }

})(jQuery);

@@ -167,19 +167,39 @@
  to 
+''
 folder
+''
  rows
+ in a table%0A  //
  to 
@@ -220,16 +220,22 @@
 d folded
+.%0A  //
 %0A  // It
@@ -1182,17 +1182,24 @@
 %3C/div%3E')
-;
+.before(
 %0A       
@@ -1203,31 +1203,11 @@
     
-var expander = a.before
+  $
 ('%3Cs
@@ -1246,16 +1246,8 @@
 n%3E')
-.prev();
 %0A   
@@ -1247,32 +1247,26 @@
 %3E')%0A        
-expander
+  
 .attr(%22title
@@ -1360,16 +1360,26 @@
 rgs); %7D)
+%0A        )
 ;%0A      
@@ -2723,154 +2723,331 @@
     
-tr.after('%3Ctr%3E%3Ctd%3E%3Cspan class=%22loading%22%3E%3C/span%3E%3C/td%3E
+var loading_row = $((%0A        '%3Ctr%3E'+%0A        ' %3Ctd class=%22$td_class%22 colspan=%22$cols%22 '+%0A        '     style=%22padding-left: $%7Bdepth%7Dpx%22%3E'+%0A        '  %3Cspan class=%22loading%22%3ELoading $entry...%3C/span%3E'+%0A        ' %3C/td%3E'+%0A        '
 %3C/tr%3E'
-);
 %0A      
-var loading_row = tr.next();%0A      loading_row.children(%22td%22).addC
+  ).replace(/%5B%5C$%5D%7B?(%5Cw+)%7D?/g, function(_,key) %7B return %7B%0A         td_c
 lass
-(
+: 
 td_class
 )%0A  
@@ -3042,17 +3042,18 @@
 td_class
-)
+, 
 %0A       
@@ -3057,24 +3057,14 @@
     
-.attr(%22colspan%22,
+ cols:
  tr.
@@ -3084,17 +3084,18 @@
 ).length
-)
+, 
 %0A       
@@ -3099,110 +3099,97 @@
     
-.css(%22padding-left%22,
+ depth:
  depth
-);
+, 
 %0A      
-loading_row.find(%22span.loading%22).text(%22Loading %22 + a.text() + %22...%22
+   entry: a.text() %7D%5Bkey%5D; %7D)%0A        );%0A      tr.after(loading_row
 );%0A 

Clarify HTML snippet for the creation of the "Loading ..." row by using less jQuery API