Problembeschreibung / Aufgabenstellung

Was in nachfolgender Abhandlung erreicht werden soll, ist relativ komplex zu beschreiben. Besser ist es sich einfach ein lebendes Beispiel anzusschauen unter www.marks-gmbh.de. Trotzdem ein kurzer Versuch die Zielstellung zu beschreiben.

1. Die Tabellenlisten

Die Selectlisten in Formularen (auch Auswahllisten oder Optionslisten) kenn nur eine recht einfache Darstellung der Optionselemente - quasi immer nur als eine Spalte. Mitunter ist genau das zu wenig. Man möchte mehrere Information vernünftig strukturiert in Spalten anzeigen. In unserem Beispiel, alternative Bezeichnung von Legierungen (unterschiedliche Normen und üblichen Herstellerbezeichnungen). Das geht natürlich nur über Tabellen, die wiederum bekommt man nicht in aufspringende Selektionslisten.

2. Instantfilter bei Volltexteingaben

Zudem können diese Listen mitunter recht lang sein, oder die vorgegebene Sortirung i.d.R. nach der ersten Spalte erschwert das Auffinden von gesuchten Einträgen. Die Suche soll erleichtert werden, in dem das Selektionsfeld gleichzeitig eine Volltexteingabe ermöglicht über den die Zeilen in der Selectionliste gefiltert und so eingeschränkt werden. Entsprechend muss bei Eingaben eine Validierung stattfinden, um das Formular nur mit einer gültigen Auswahl abschicken zu können.



 

Nervend ist, dass das Eingabefeld, da es ja ein ganz normales Input-Feld ist, sich prinzipiell auch so verhält, und z.B. bei Eingaben die Eingabehilfe aktiviert und eine Liste mit historischen Werten aufspringen lässt. Diese Liste mit alten Werten überblendet leider unsere Pull-Down-Tabellenliste. Das muss unterdrückt werden, was jedoch nicht so einfach geht.
Nachtrag: Eine Lösung für dieses Problem wurde später gefunden und ist beschrieben in Drop-Down-Selectlisten mit Instant-Filter und mehrspaltiger Tabellenliste.

 

Umsetzung im Detail

Ein paar prinzipielle Festlegungen

  • Wir benötigen zur Umsetzung das Formular mit unserem neuen Listen-Element, ein CSS-Datei oder eingebettet im Formular, und unser JavaScript als Datei oder eingebettet im Formular.
  • Unser neues Formular-Objekt besteht aus zwei wesentlichen sichtbaren Elemente, einem Eingabefeld bezeichnen wir es mal mit dem Arbeitstitel jQueryChooser so wie auch den Style-Selector für diese Element und zum zweiten der Auswahltabelle, ebenfalls wie sein Style-Selector benannt als SelectTblBox.
  • Zu diesen beiden Elemeten gibt es mindestens noch je ein Hidden-Imput-Feld.

 

Das CSS-Gerüst zur Formatierung des Listenelementes

Beginnen wir mal mit den einfachen Dingen, die wenig Erklärung bedürfen - der CSS-Datei mit den Styleanweisungen für unser Listen-Element.

.SelectTblBox {
  position:absolute;
  visibility:hidden;
  height:0;
  overflow-x:hidden;
  overflow-y:auto;
  border:1px solid #80A0D3;
  background: #FFF;
}

.jQueryChooser {
 width:160px;
 cursor:pointer;
 background: #fff url('/images/jQueryChooser.gif') no-repeat 124px 0;
}

.jQueryChooser.curractive {
 cursor:text;
 background-position: 124px -18px;
}

.SelectTblBox table { margin:0 16px 0 0; }
.SelectTblBox tr { cursor:pointer; }
.SelectTblBox tr td { padding-left:5px; }
.SelectTblBox tr.hovercell { background: #E5EFF7 url('/images/jQueryChooser.gif') no-repeat 0 -35px ; }
.SelectTblBox tr.currentcell { background-color:#CCE0F0; }

.blueBtn, a.blueBtn, .FormRow input.blueBtn {
  position: relative;
  color:#FFF !important;
  background-color: #0066b3;
  border:#cce0f0 2px solid;
  color: #FFF; box-shadow:
  2px 3px 3px 0 #666;
  border-radius: 5px 5px 5px 5px;
  padding:3px 1em; margin: 0 6px 6px 0;
  font-weight:bold;
  text-decoration:none !important;
  text-align:center;
}

.blueBtn:hover, .FormRow input.blueBtn:hover, .FormRow input.blueBtn:active, .FormRow input.blueBtn:focus,
a.blueBtn:hover, a.blueBtn:active, a.blueBtn:focus {
  border-width:2px;
  background-color:#80A0D3;
  border-color:#0066B3;
  text-decoration:none; color:#FFF;
}

.blueBtn.disabled, .FormRow input.blueBtn.disabled {
  color:gray;
  border-color:gray;
  background-color:lightgray;
}

Dies sind nur die wichtigsten Formatanweisungen, die für die Funktion des Listen-Elements notwendig sind.

 

Das Formular mit dem Listenelement

Hier das Formularelement für unser Beispiel:

$MatSelField = "form_marks_".$TplType."_ma_sel";
$Material->sel =
    vmRequest::getString($MatSelField,
    vmRequest::getString("_ma", $objUrlParams->Legierung,'GET'),'GET');
<form method="post" class="addtocart_form"
  action="<?php echo $mm_action_url ?>index.php"
  name="<?php echo $TplType ?>_form"
  id="<?php echo $TplType ?>_form"
<?php
  $ajaxLink = "javascript:loadNewForm('vmMainPage','\index2.php?".$urlOpt."','".$TplType."_form');return false;";
  echo 'onsubmit="'.$ajaxLink.'" action="" ' ;
  $onChangeAjax = 'onchange="'.$ajaxLink.'"';
  $onClickAjax = 'onclick="'.$ajaxLink.'" onkeyup="'.$ajaxLink.'"';
?>
>
/*
* ... einige Formularfelder ...
*/
<?php $uniqueID = '_'.time();
?>
  <div id="MaterialFormRow" class="FormRow" style="<?php echo $rowWidth ?>">
    <label for="<?php echo $MatSelField.$uniqueID ?>" class="_lbl"><?php echo $VM_LANG->_('OWN_MAT') ?>:</label>
    <div class="_val">
        <div id="selectBtnFld">
          <input type="text" read_only="readonly"
            value="<?php echo $Material->sel; ?>"
            id="<?php echo $MatSelField.$uniqueID ?>"
          name="<?php echo $MatSelField.$uniqueID ?>"
          onfocus="toggleTblSelectList('RZ_form','<?php echo $MatSelField.$uniqueID; ?>');"
          class="jQueryChooser"
                  />
          <?php echo vmToolTip($VM_LANG->_('OWN_LEG_JQUERY_CHOOSER')); ?>
        <span id="helpinfo_<?php echo $MatSelField.$uniqueID ?>"> </span>
        <input type="hidden"
            value="<?php echo $Material->sel; ?>"
          id="<?php echo $MatSelField.$uniqueID; ?>_post"
          name="<?php echo $MatSelField; ?>"
          />
      </div>
      <div
          id="<?php echo $MatSelField.$uniqueID ?>_list"
        name="<?php echo $MatSelField.$uniqueID ?>_list"
          class="SelectTblBox">
          <table id="tbl"><tbody id="tblBody">
          <?php
            foreach ($Material->Items as $mat) :
              ?>
              <tr id="<?php echo $MatSelField.$uniqueID."_".$mat->leg; ?>"
                  title="<?php echo $mat->leg; ?>" <?php if ($Material->sel == $mat->leg ) : ?>class="currentcell"<?php endif; ?>>
                <td><?php echo $mat->leg; ?></td>
                <td><?php echo $mat->bezeichng; ?></td>
                <td><?php echo $mat->iso_code; ?></td>
              </tr>
              <?php
            endforeach;
          ?>
          </tbody></table>
            <input id="<?php echo $MatSelField.$uniqueID ?>_url" type="hidden" value="/index2.php?<?php echo $urlOpt; ?>" />
      </div>
        </div>
    </div>
/*
* ... einige weitere Formularfelder ...
*/
  <input
    style="float:right;" id="CalcBtn"
    type="submit" title="Preis kalkulieren"
    value="Preis kalkulieren" class="button blueBtn" />
</form>

Zum Verständnis hier die Erläuterungen:

  • Dieses Bsp. ist eingebettet in Joomla und VirtueMart. Folglich beziehen sich einige Variablen und Methodenbezeichner auf deren FrameWorks, die hier nicht weiter erklärt werden.
  • Für alle anderen Formularelement wird bei Änderungen immer das Formlar über die Ajax-Funktion loadNewForm() aktualisiert. Unser neues Listenelement-Eingabefeld ruft hingegen eine eigene neue Funktion toggleTblSelectList() auf und übergibt dabei die zwei Parameter - für den Formular- und den Feld-Namen. Diesem sichtbaren Eingabefeld wird eine Unique-ID an den Namen gehängt. Das ist notwendig für das Unterdrücken der Eingabehilfe mit historischen Werten. Für dieses Feld kann es keine historischen Werte geben, die folglich auch nicht eingeblendet werden.
  • Weiterhin gibt es ein bis auf die angehängte Unique-ID gleichlautendes Hidden-Feld. Während das o.g. nur für der User unique zur Eingabe dient, wird dieses Feld tatsächlich durch das PHP-Script ausgewertet. Beide Felder enthalten beim Absenden den gleichen Wert.
  • Wichtig für die Funktion der Tabellenzeilenauswahl ist, dass die Tabellenzeilen ein Attribut title erhalten! Dieses Attribut wird durch unser JS ausgelesen und simuliert quasi das in Selektionslisten übliche Option-Value-Attribut. Unsere Tabellenzeile erhält außerdem einen Selector zum Kennzeichnen des aktuellen Elementes (currentcell). Die Foreach-Schleife der Tabelle wird für jede gefundene Legierung durchlaufen und so je eine Auswahlzeile erzeugt.
  • Im Formular ist zu erkennen, dass der gesamte Satz zusammengehöriger Felder immer identivizierbar sind über ihre IDs die sich lediglich unterscheiden durch die Anhänge _list, _post, und _url bzw. bei den Tabellenzeilen durch den gleichen Inhalt wie der title der Zeilen. Im JavaScript sind die Felder so konkret ansprechbar.

 

Das JavaScript mit den Hauptfunktionen toggleTblSelectList() und selectTblRow()

Kommen wir nun zum eigentlichen Herzstück, der DropDown-Listenfunktion. Hierfür gibt es die beiden genannten Hauptfunktionen, wobei in die erste noch ein paar Hilfsfunktionen eingebettet sind und die zweite durch das erste an die Tabellenzeilen als Clickereignis gebunden werden.

Hier nun das Script gefolgt von ein paar Erläuterungen:

var form_valid = false;
function checkRZ_form() { return form_valid; }
function toggleTblSelectList (form, fld) {
    document.getElementById(fld+'_list').style.visibility = 'visible';
    var tblListRow = jQuery('#'+fld+'_list tr');
    jQuery('#CalcBtn').addClass('disabled').attr('disabled','disabled');
    slideListHight();
    jQuery('#'+fld+'_list tr')
    .mouseenter( function() { jQuery(this).addClass('hovercell') })
    .mouseleave( function() { jQuery(this).removeClass('hovercell') })
    .off('click').click( function() { selectTblRow( jQuery(this).attr('id'), form, fld )    });
    inFld = jQuery("#"+fld)
    .focusout( function() { validateSelection()    })
    .addClass('curractive')
    .keyup(function(event) {
        writeHlp('');
        var name = inFld.val();
// writeHlp(event.which + "->" + event.keyCode);
        if(event.keyCode > 40 || event.keyCode < 91  || event.keyCode ==  8 || event.keyCode == 13 ||
            event.keyCode == 20 || event.keyCode == 16 || event.keyCode ==  9 ||
            event.keyCode == 37 || event.keyCode == 38 || event.keyCode == 39 ||
            event.keyCode == 40 || event.keyCode == 52 /* Enter */)
        {
            rowcount = 0;
            var chars = name.length;
            var rowTitle = '';
            tblListRow.each( function(i) {
                value = '';
                jQuery(this).children().each( function(j) {
                    value += jQuery(this).text()+' ';
                })
                // Zeilen ausblenden
                if (value.toUpperCase().search(name.toUpperCase()) != -1) {
                    jQuery(this).show();
                    rowTitle = jQuery(this).attr('title');
                    rowcount++;
                } else {
                    jQuery(this).hide();
                }
                writeHlp(rowcount + " " +rowTitle);
            });
        } else {
            writeHlp('Unzul&auml;ssiges Zeichen!');
        }
        if (rowcount == 1) { // Wenn nur ein Treffer, dann sofort Formular absenden
            selectTblRow( fld + '_' + rowTitle, form, fld);
        } else {
            slideListHight();
        }
    });    
    function slideListHight() {
        var rowHeight = tblListRow.last().height();
        var maxListRows = 6;
        var maxListHeight = rowHeight * maxListRows;
        var container = jQuery('#'+fld+'_list')
        var tblList = jQuery('#'+fld+'_list table');
        currListHeight = tblList.height();
        currListWidth = tblList.width();
        if (currListHeight == 0) {
            container.animate( {height : '0', width : inFld.width(), opacity : 0 },duration );
            jQuery("#"+fld).addClass('error');
// writeHlp(tblList.height());
            document.getElementById("FormErrMsgs").innerHTML = "<ul><li>Dieser Suchtext erzeugt keine Resultate.</li></ul>";
            document.getElementById("FormErrMsgs").className = 'error';
        } else {
            jQuery("#"+fld).removeClass('error');
            document.getElementById("FormErrMsgs").innerHTML = "";
            document.getElementById("FormErrMsgs").className = '';
            if (currListHeight > maxListHeight) {
                container.animate( {height : maxListHeight+2+'px', width : currListWidth+2+'px', opacity : 1 }, duration    );
            } else {
                container.animate( {height : currListHeight+2+'px', width : currListWidth+2+'px', opacity : 1 }, duration    );
            }
        }
    }
    function writeHlp(text) {    jQuery("#helpinfo_"+fld).empty().html(text).addClass('err').show(); }
    function validateSelection() {
        var v = aV = '';
        var n = inFld.val();
        form_valid = false;
        tblListRow.each( function() {
            v = jQuery(this).attr('title');
            if (v == n) form_valid = true;
        });
        if (!form_valid) {
            document.getElementById("FormErrMsgs").innerHTML = "<ul><li>&quot;"+ n + replaceUml("&quot; ist keine g&uuml;ltige Auswahl f&uuml;r eine Legierung. Bitte eine Auswahl treffen.") + "</li></ul>";
            document.getElementById("FormErrMsgs").className = 'error';
            jQuery("#"+fld).addClass('error');
        }
    }
}
function selectTblRow (rowId, form, fld) {
    form_valid = true;
    rowTitle = document.getElementById(rowId).getAttribute('title');
    jQuery('#'+fld).val(rowTitle); // ins sichtbare Eingabefeld Leg.Auswahl eintragen
    jQuery('#'+fld+'_post').val(rowTitle); // ins sichtbar Eingabefeld Leg.Auswahl eintragen
    jQuery('#'+fld+'_list').css('visibility','hidden'); // Auswahlliste wieder verstecken
    loadNewForm('vmMainPage',jQuery('#'+fld+'_url').val() ,form);
}

Wie oben schon erwähnt, wird die Funktion getriggert durch einen Klick auf das sichtbare Eingabefeld unseres Drop-Down-Listenelementes. Die Variable form_valid wird global befüllt, damit diese in den Funktionen zur Verfügung steht. Der Ablauf ist dann wie folgt:

  • Es wird zunächst per jQuery eine Liste der Tabellenzeilen in tblListRow abgelegt.
  • Dann wird der Submit-Button (hier CalcBtn) deaktiviert um ein vorzeitiges Absenden ohne getroffene Auswahl zu verhindern.
  • dann wird die Unterfunktion slideListHeight() aufgerufen. Diese Funktion sorgt dafür, dass per jQuery-Animation unsere Tabelle als Drop-Down-Fensterchen aufgezogen wird. Die Höhe/Breite stellt sich dabei automatisch so ein wie es Treffer gibt unter Berücksichtigung eines voreingestellten Maximalwertes für die Anzahl der sichtbaren Zeilen (maxListRows). Diese Funktion erzeugt auch eine Fehlerausgabe, wenn die Tabelle keine Zeilen hat, also es keine Treffer gibt, was ausgeschlossen werden muss beim Absenden des Formulars.
  • Dann werden an alle Tabellenzellen zum einen MouseEnter- und MouseLeave-Ereignisfunktionen gebunden um Style-Selektoren hinzuzufügen oder zu entfernen. Zum anderen wird als Click-Ereignisfunktion die Funktion selectTblRow() angebunden. Zu dieser Funktion unten mehr.
  • Damit ist eigentlich schon das Verhalten unserer Tabellenzeilen beim Aufruf abgeschlossen.
  • Weiter folgt jetzt das Verhalten unseres Eingabefeldes, um die Instant-Filter-Funktion d.h. die Einschränkung unser anzuzeigenden Tabellenzeile zu ermöglichen. Mit jQuery('#'+fld) bauen wir uns zunächst unsere jQuery-Objektreferenz an die wir dann diverse Methoden hängen.
  • Zunächst wird die Funktion validateSelection() aufgerufen, sobald das Eingabefeld seinen Focus verliert. In dieser Funktion (weiter unten im Script) machen wir beim Durchlaufen aller Tabellenzeilen anhand o.g. title-Attributes eine Prüfung, ob es einen gleichlautenden Wert gibt. Den muss es geben ansonsten erfolgt eine Fehlermeldung und valid_form erhält nicht den Status true, sondern false. Das Absenden wird in Folge verhindert.
  • Dann weisen wir den Selector currentacive zu, um es seinem aktuellen Status entspr. hübsch zu formatieren. In meinem Formular ändert sich das Dreieck der üblichen DropDown-Buttons in einen Edit-Stift, um dem User zu zeigen, dass er Volltext eingeben darf.
  • Nun wird die wichtige Funktion eingebunden, die bei der Eingabe von Tastaturzeichen (Ereignis keyup) permanent inkl. einiger Validierungen mit dem sich ergebenden Filter-Suchtext die Tabellenzeilen ein- und ausblendet. Gleichzeit mit dem jedem Ein-/Ausblenden von Tabellenzeilen wird wieder die Funktion slideListHight() aufgerufen, um bei Notwendigkeit die Tabellenbox-Ausdehnung anzupassen.
  • Eingefügt wurde noch eine zusätzliches Verhalten, nämlich dass das Formular sofort aktualisert wird, sobald festgestellt wurde, dass nur noch exakt eine Trefferzeile aus der Tabelle übrig geblieben ist. Denn dann kann das als gültige Auswahl akzeptiert werden und der Wert damit vervollständigt in das Eingabefeld eingetragen werden.

Soweit zu dieser Funktion. Nun noch ein paar Aussagen zur Funktion selectTblRow():

Diese wird ausgeführt sobald der User durch Klicken auf eine Tabellenzeile eine Auswahl tritt.

  • Wir wissen dann schon erstmal, dass das nur ein gültiger Wert sein kann und setzten deshalb form_valid auf true.
  • Dann ermitteln wir den Wert der Auswahl über unser als value funktionierende title-Attribut.
  • Dieses tragen wir ein in den beiden o.g. Input-Felder - in das sichtbare für den user und in das unsichtbare für unser Formular-Request, um es im php-Script auswerten zu können.
  • Dann verstecken wir unsere Auswahltabelle, was aber eigentlich nicht zwingend notw. ist, weil das Formular eh per Ajax neu geladen wird.
  • Aus unserem Hidden-Feld url holen wir unseren Ajax-Aktualisierungs-URL und rufen die Ajax-Funktion loadNewForm() auf, die unser Formular mit neuen Werten aktualisiert.