jQuery-Selektor-Erweiterung Labeled

Für eine TYPO3-Website soll ein E-Mail-Formular eingerichtet werden. Ich nutze die Erweiterung Powermail, die mir alle wesentlichen Schritte abnimmt: Nur die einzelnen Felder müssen angegeben werden, um Formulargenerierung und Mail-Versand muss man sich nicht kümmern. Für ein paar zusätzliche Validierungen nutze ich Javascript, das die entsprechenden Felder des Formulars anhand ihrer IDs identifiziert und validiert. Nachdem auf dem Testsystem alles funktioniert, exportiere ich die Seite in die Live-Website. Dort der Schock: Alle Feld-IDs haben sich geändert, das Javascript funktioniert nicht mehr.

Powermail und das UID-Problem

Die Ursache ist Powermails UID-Nutzung: Formularfelder werden in einer Datenbanktabelle gespeichert, die eine Auto-Increment-ID enthält. Diese UID wird im HTML auch für ID- und Name-Attribut des Feldes verwendet. Beim Export stimmt dieser Identifikator oft nicht mehr überein. Nachdem mir dies nun gefühlte 20 mal (also vermutlich zwei mal) passiert ist, muss eine Lösung her.

Der menschliche Weg: Beschriftungen folgen

Die Idee: Felder nicht anhand eines unberechenbaren Attributs identifizieren, sondern den menschlichen Ansatz wählen: Man gibt an, wie ein Formularelement beschriftet ist.

Mein kleines Projekt Labeled bringt dem jQuery-Selektor diesen Ansatz bei. Die Zusatzfunktion :labeled(suchstring) sucht nach interaktiven DOM-Elementen, die in irgend einer Form durch den entsprechenden String beschrieben werden.

  • Beliebige Elemente, die durch das For-Attribut eines <label for="refid">suchstring</label> referenziert werden
  • Schaltflächen (<button>, <input type="submit|reset">, <a>), die mit suchstring beschriftet sind
  • Fieldsets, die ein direktes Kindelement <legend>suchstring</legend> enthalten

Die Funktion lässt sich in der üblichen jQuery-Selektorsyntax verwenden, z.B. $('button:labeled(Abschicken)'), $('fieldset:labeled(Persönliche Daten) input:labeled(Vorname)') oder einfach $(':labeled(E-Mail-Adresse)').

Labeled auf GitHub

Die jQuery-Selektor-Erweiterung Labeled kann auf der GitHub-Seite des Projekts heruntergeladen werden: https://github.com/SebastianG86/jquery-labeled

Weiterführende Gedanken

Schon während der Entwicklung einer Browser-Automatisierung im Rahmen meiner Masterarbeit bin ich auf viele Webdienste gestoßen, die den Quelltext ihrer Seiten derart generieren, dass eine Selektion schwer fällt. Es werden oft nur Divs oder Spans verwendet, die anhand von zufälligen IDs und Klassen Funktion und Aussehen erhalten. Semantik ist in einem solchen Markup kaum zu finden.

Zu Demonstrationszwecken wollte ich aus einem sozialen Netzwerk automatisiert die Wohnorte der Kontakte auslesen. Diese Orte standen jeweils in einer Tabelle, deren Länge je nach Anzahl der angegebenen Informationen variierte. Das gesuchte Datum war daher mal in der dritten, mal in der fünften Zeile. Da sich auch Klassen und ID der Zelle stets änderten, war eine Adressierung kaum möglich.

Auch in diesen Fällen ist ein beschriftungsorientierter Ansatz sehr hilfreich. Dieser müsste jedoch viel weiter gehen, als es Labeled derzeit erlaubt. Formulierungen wie „die Zelle rechts neben ‚Wohnort’“ erfordern eine weit umfangreichere Syntax und Logik.

Realisierung

Die Erweiterung besteht fast ausschließlich aus einer großen Bedingung. Lediglich die Tests für referenzierende Labels und Legends wurden in Funktionen verschoben – und selbst das hat eigentlich nur optische Gründe.

(function($) {
  $.extend($.expr[':'], {
    labeled: function(element, b, args) {
      var labelText = args[3];
      element = $(element);
      return element.is('a:contains("' + labelText + '")')
        || element.is('button:contains("' + labelText + '")')
        || element.is('input[value="' + labelText + '"]')
        || element.is('*[placeholder="' + labelText + '"]')
        || elementHasReferencingLabel(element, labelText)
        || elementHasLegend(element, labelText);
    }
  });

  function elementHasReferencingLabel(element, labelText) {
    return element.is('[id]') 
        && $('label[for=' + element.attr('id') + ']:contains(' + labelText + ')').length;
  }

  function elementHasLegend(element, labelText) {
    return element.is('fieldset') 
      && element.find('> legend:contains(' + labelText + ')').length;
  }

})(jQuery);