Tabellen per Javascript filtern

Wenn Google versagt… muss man selber ran! Jedenfalls fand ich zu meinem konkreten Problem keine wirkliche Lösung.
Was ist das Problem? Nun, ich will eine Tabelle filtern, mit Javascript. Soweit kein Problem, dazu findet man unzählige Lösungen, z.B. http://www.vonloesch.de/node/23.

Nur versagen diese Lösungen alle, wenn man in einer Tabelle Zellen über mehrere Zeilen zusammengefasst hat.

Wenn der Suchbegriff in einer der Zeilen ist, die „unter“ einer Zeile mit rowspan liegen, diese Zeile also weniger Zellen hat (technisch, optisch natürlich nicht) und alle anderen
Zeilen ausgeblendet werden, dann hat man plötzlich nicht mehr drei Zellen, sondern z.B. nur noch eine:table2

Dieser Fall ist ja sogar noch ertragbar, blöd wird es wenn da noch andere Zeilen dazu kommen und die Tabelle auf einmal so ganz anders aussieht…table3

Was also tun? Prinzipiell sollte es ja so sein, das Zeilen die über rowspan verbunden sind, auch zusammen gehören. Daher wäre es ja vielleicht ganz gut, wenn man diese Zeilen auch alle anzeigen würde auch wenn der Suchbegriff nur in einer der Zeilen auftaucht? Also so: table4

Einen Teil meiner Lösung habe ich bei Stackoverflow gefunden. Die Grundlage bildete ein Dokuwiki-Plugin, von dessen Javascript ist aber eigentlich nichts mehr übrig geblieben. Da ich das für ein Dokuwiki brauche, gibt es ein paar „Seltsamkeiten“, wie das ich jQuery anstatt $ schreiben muss.
Meine Lösung hat auch nicht alle Fälle berücksichtig, bei komplizierteren Konstrukten dürfte das sicher aussteigen. Da diese aber über die verfügbare Wikisyntax nicht herstellbar sein dürften, ist das hinreichend gut. 😉

  1. searchtable = {
  2.   filterall : function(term, _id) {
  3.     var searchstr = term.value.toLowerCase();
  4.     var table = jQuery(jQuery('#' + _id + ' table')[0]); // just our table
  5.     var tds = table.find('td'); // all td
  6.     var trs = table.find('tr'); // all tr
  7.     var first = jQuery(trs[0]); // first row should be our header
  8.     var tdc = first.children().length; // count the number of ths in first row
  9.  
  10.     // remove all old search hits
  11.     jQuery('span.search_hit').each(function() {
  12.       jQuery(this).replaceWith(jQuery(this).text());
  13.     });
  14.  
  15.     // show everything if searchstr is empty
  16.     if (searchstr.length == 0) {
  17.       trs.show();
  18.       return;
  19.     }
  20.  
  21.     // mark each searchstr
  22.     var re = new RegExp("(" + searchstr + ")", "ig");
  23.     tds.filter(function() {
  24.       return this.innerHTML.toLowerCase().indexOf(searchstr) >= 0
  25.     }).each(function() {
  26.       if (jQuery(this).children().length < 1) {
  27.         jQuery(this).html(
  28.           jQuery(this).text().replace(re,'<span class="search_hit">$1</span>')
  29.         )
  30.       }
  31.     });
  32.  
  33.     // find all tds which contain the searchstr, case insensitive
  34.     var toShow = tds.filter(function() {
  35.       return this.innerHTML.toLowerCase().indexOf(searchstr) >= 0
  36.     });
  37.  
  38.     // hide all rows with no results
  39.     trs.not(trs.has(toShow)).hide();
  40.  
  41.     // always show first row!
  42.     first.show();
  43.  
  44.     // do some magic for rowspans
  45.     // we want to show each rowspan totally, so our table doesnt break anymore
  46.     toShow.each(function() {
  47.       var father = jQuery(this).parent();
  48.  
  49.       // if this is a "child" of an rowspan-tr, try to find our "parent"
  50.       // find first tr above our tr which has an rowspan-td
  51.       if (father.find('td').length < tdc) {         
  52.         father = father.prevAll('tr').find('td[rowspan]').eq(0).parent();       
  53.       }    
  54.  
  55.       // find all td with rowspan        
  56.       var rspan = father.find('td[rowspan]');        
  57.       if (rspan.length) {          
  58.         // ok, just use only the first one... there may be more, but we simply ignore them          
  59.         rspan = parseInt(jQuery(rspan[0]).attr('rowspan'));          
  60.         father.nextUntil(':nth-child(' + (father.index() + rspan + 1) + ')').andSelf().show();
  61.       }
  62.     });
  63.   },
  64. };

Der Code ist kommentiert (damit ich den auch noch in 3 Monaten blicke… 😉 ), trotzdem kurz erklärt:

Zeile Beschreibung
3-8 Ein paar Initialisierungen
10-12 Entferne alle alten Suchmarkierungen
15-18 Wenn es keinen Suchstring gibt mach alles sichtbar und mach Schluss
21-30 Markiere alle Vorkommen des Suchstrings
33-35 Suche alle td die den Suchstring enthalten
38 Verstecken alles andere
41 Da die erste Zeile immer der Header ist: zeige sie immer an
46 Gehe durch alle gefundenen td
47 Hol das Elternelement des aktuellen td, sollte ein tr sein
51 Wenn das aktuelle tr weniger td hat als die erste Zeile ist das vermutlich einem rowspan geschuldet
52 „Spule“ durch alle tr zurück, bis du eins findest, das ein td mit rowspan enthält.
55 Hole alle td mit rowspan aus dem aktuellen tr, das muss nicht mehr das tr-Elternelement des aktuellen td sein
58 Hol dir die Anzahl an Zeilen über das sich dieser rowspan spannt
59 Ausgehend von der aktuellen Position gehe die gefundene Anzahl an Zeilen runter und lasse all diese Zeilen wieder auftauchen

Kommentare erwüscht! Da ist sicher noch Optimierungspotential drin. 😉