Archiv für den Monat: Mai 2018

WordPress Bewertungsformular mit AJAX

Einleitung

Sie kennen wahrscheinlich mein Bewertungsformular, es steht am Ende jedes Artikels und erlaubt eine Benotung von sehr gut bis ungenügend. Bislang ist es so, dass ich eine Nachricht erhalte, wenn jemand eine Bewertung abgeschickt hat, und die Information nur für mich privat relevant ist. Jetzt dachte ich mir, das geht auch anders: erstens möchte ich am Ende jedes Artikels angezeigt bekommen, ob schon Bewertungen vorhanden sind. Und zweitens soll sich die Anzeige automatisch aktualisieren, wenn jemand eine neue Bewertung abgibt. Dazu brauchts a bisserl AJAX, und das geht mit WordPress eigentlich ganz einfach, wenn man das Prinzip einmal überrissen hat.

Formular am Ende jedes Artikels anzeigen

Dafür basteln wir uns ein Plugin, und in das kommt als Erstes mal das Formular und ein Filter, der das ganze ans Ende jedes Contents hängt. Wahlweise kann man das auch in eine Abfrage klemmen, die das Formular nur in der Einzelansicht anzeigt, das fragt man mit dem if(is_single() ab. Sonst ist nichts weiter dabei. Ansonsten gibt es 6 Radiobuttons mit den Noten und einen Button zum absenden des Formulars.

function el_insertText($content) {
    echo $content."<br><hr>";
  // if(is_single()) {
      
      echo '<div style="border:1px dotted #000; text-align:center; padding:10px;">';
      echo '<h4>Dir gefällt dieser Beitrag?</h4>';
      echo '<p>Dann bewerte ihn!</p>';
      echo '</div>';
      
     echo " <form action = '' method='post'>";
  
      echo "<fieldset>
        <div id = 'noten' class = 'noten' style = 'border:2px solid blue; padding :4px;'>
        <input type='radio'  name='note' value='1'>sehr gut
        <input type='radio' name='note' value='2'>gut
        <input type='radio' name='note' value='3'>befriedigend<br>
        <input type='radio'  name='note' value='4'>ausreichend
        <input type='radio' name='note' value='5'>mangelhaft
        <input type='radio' name='note' value='6'>ungenügend
        
        <input type ='button' name = 'absenden' class = 'bew-button' value = 'abschicken'>
        </div>
      </fieldset>";
    echo "</form>";
          
  // } //End von is_single
  
}
add_filter ('the_content', 'el_insertText');

Was brauchen wir für die Verarbeitung?

Eine Tabelle zum Speichern der Noten

Erstens eine Tabelle, in der die Bewertungen gespeichert werden. Ich hab mir da eine kleine Lösung im PHPmyAdmin angelegt, die Tabelle heisst bewertungen und hat drei Felder, die id als Autowert, die beitrags_id und die note.

tabelle_bewertungen

tabelle_bewertungen

WordPress-Puristen schreien hier wahrscheinlich schon Zetermordio, man könnte natürlich die Bewertungen in die wp_options reinklemmen, aber das war mir jetzt zu umständlich, ich arbeite lieber mit einer eigenen Tabelle.

Die aktuelle Beitrags-ID

Zweitens brauchen wir die ID des bewerteten Beitrags, und drittens natürlich die vergebene Note. Die ID holt man sich mit get_the_ID(), und ich schreibe sie als unsichtbaren Text mit rein, das geht so:

echo "<span id = 'beitragsid' style='display:none;'>".get_the_ID()."</span><br>";

Dann kann ich mir nachher anhand der Span-id im Javascript die ID herauspflücken. Die Note hole ich mir auch mit dem Script, dazu gleich mehr.

Eine Logik zur Ausgabe der bereits vorhandenen Bewertungen

Drittens brauchen wir für die Anzeige der schon vorhandenen Bewertungen eine Abfrage auf unsere Tabelle bewertungen und eine Ausgabe der berechneten Durchschnittsnote sowie der Anzahl der vorhandenen Bewertungen. Das ist nicht weiter wild, mit $wpdb und der BeitragsID ist das fix erledigt.

global $wpdb;
    $alleposts = $wpdb->get_results( "SELECT * from bewertungen 
    where beitrags_id = ".get_the_ID()."");
    
    $gefunden = $wpdb->num_rows;
        
    $durchschnitt = 0;
    foreach($alleposts as $einpost){
        $durchschnitt = $durchschnitt + $einpost->note;
    }
    
    if ($gefunden != 0){        
    $durchschnitt = $durchschnitt / $gefunden;
    }
        
    $durchschnitt= round($durchschnitt);

Damit aus dem gerundeten Notenwert auch ein Wort wird, klemmen wir noch einen switch case dahinter:

    $wort = '';
switch ($durchschnitt) {
    case 0:
        $wort = "keine Bewertung";
        break;
    case 1:
        $wort = "sehr gut";
        break;
    case 2:
        $wort =  "gut";
        break;
    case 3:
        $wort = "befriedigend";
        break;
    case 4:
        $wort =  "ausreichend";
        break;
    case 5:
        $wort =  "mangelhaft";
        break;
    case 6:
        $wort =  "ungenügend";
        break;
}
    
    echo "<span id = 'ausgabe'>".$gefunden." Bewertungen gefunden, Durchschnitt: ".$wort."<span>";

Die Ausgabe-Span kriegt gleich noch eine id verpasst, hier schreiben wir nachher unsere aktualisierten Werte rein.

Das zugehörige Javascript

…kommt mit ins Plugin-Verzeichnis, und wird mit enqueue script geladen. Wir schicken mit dem wp_localize_script gleich noch die URL zur admin-ajax.php mit.

add_action( 'wp_enqueue_scripts', 'el_bew_assets' );
function el_bew_assets() {
    
        wp_enqueue_script( 'bew', plugins_url( '/el_bewertung.js', __FILE__ ), array('jquery'), '1.0', true );
        wp_localize_script( 'bew', 'bewertungen', array(
        'ajax_url' => admin_url( 'admin-ajax.php' )));

    }

Wir packen unser gesamtes Script in eine document-ready Function, weil sonst JQuery noch nicht geladen sein könnte, wenn wir unser Script ausführen.  Die Function rennt los, sobald jemand den Bewertungs-Button geklickt hat. Zuerst werden die benötigten Daten auf Variable gelegt, und dann noch abgecheckt ob auch eine Benotung anggeklickt wurde.

jQuery(document).ready(function($){
jQuery( document ).on( 'click', '.bew-button', function() {
            
            
            // Ajax url aus localize script
            var a_url = bewertungen.ajax_url;
                        
            //Note aus den Radiobuttons holen
            var selectedOption = $("input:radio[name=note]:checked").val();
            
            //Beitragsid aus span holen
            var akt_id = document.getElementById("beitragsid").innerHTML;
            
            //Check ob keine Note angewählt, Abbruch
            if (typeof selectedOption === 'undefined'){
            alert('Bitte eine Note wählen!');
            return;}
                
            var post_note = selectedOption;
            
            alert('Vielen Dank, ihre Benotung wurde gespeichert');
            
            //Ajax-Call        
            jQuery.ajax({
                url : a_url,
                type : 'post',
                data : {
                    action : 'bew_add_bewertung',
                    post_id : akt_id,
                    note : post_note
                },
                success : function( response ) {
                    document.getElementById("ausgabe").innerHTML = response;
                
                }
    });
            
})
});

Der AJAX-Call ist recht einfach, es wird die ajax-admin-URL mitgegeben und als Request-Typ ‚post‘ definiert. In den data schicken wir zuerst mit, welche Funktion ausgeführt werden soll (dazu gleich mehr), und übergeben die Beitrags-ID sowie die Note. In der success-Function schreiben wir den Rückgabewert des Calls in die Span mit der ID ausgabe. Das wars schon!

Jetzt müssen wir WordPress nur noch anweisen, die Callback-Function auch zuzuordnen, dazu ergänzen wir unser Plugin um folgende Zeilen:

add_action( 'wp_ajax_nopriv_bew_add_bewertung', 'bew_add_bewertung' );
add_action( 'wp_ajax_bew_add_bewertung', 'bew_add_bewertung' );

Das erste Argument mit dem nopriv gilt fürs Frontend, das Zweite könnte ich mir eigentlich sparen, das wäre für die Admin-View und steht hier nur der Vollständigkeit halber.

Die Callback-Funktion

Macht ganz etwas Ähnliches wie unsere Bildschrimausgabe von oben, sie ermittelt anhand der übergebenen Beitrags-ID und der Note die aktuellen Zahlen. Dafür wird zuerst einmal die neue Bewertung in die Tabelle bewertungen eingetragen (Insert), dann wird neu berechnet und ausgegeben. Den switch case könnte man auch in eine eigene Funktion auslagern, ich habs mit copy&paste gemacht, ist genau dasselbe wie oben.

function bew_add_bewertung(){
    
    global $wpdb;
    
    //Neue Benotung eintragen
    $wpdb->insert('bewertungen', array(
            'beitrags_id' => $_POST['post_id'],
            'note' => $_POST['note']
            ));
            
    //gtNeue Anzahl holen
        $alleposts = $wpdb->get_results( "SELECT * from bewertungen 
    where beitrags_id = ".$_POST['post_id']."");
    
    $anzahl = $wpdb->num_rows;
    $durchschnitt = 0;
    foreach($alleposts as $einpost){
        $durchschnitt = $durchschnitt + $einpost->note;
    }
    
    if ($anzahl != 0){        
    $durchschnitt = $durchschnitt / $anzahl;
    }
    
    $durchschnitt= round($durchschnitt);
     $wort='';
    switch ($durchschnitt) {
    case 0:
        $wort = "keine Bewertung";
        break;
    case 1:
        $wort = "sehr gut";
        break;
    case 2:
        $wort =  "gut";
        break;
    case 3:
        $wort = "befriedigend";
        break;
    case 4:
        $wort =  "ausreichend";
        break;
    case 5:
        $wort =  "mangelhaft";
        break;
    case 6:
        $wort =  "ungenügend";
        break;
}
    
    $rueckgabe = $anzahl." Bewertungen, Durchschnittsnote: ".$wort;    
    echo $rueckgabe;
    
    die();
    
    
} //ende function bew_add_bewertung

Das die() am Ende braucht WordPress, um den Call korrekt auszuführen. Aber damit sind wir auch schon fertig. Die bereits vorhandenen Bewertungen werden angezeigt:

bewertungformular1

bewertungformular1

Wenn jetzt ein Besucher eine Bewertung abgibt, wird die Anzeige nach Klick auf den absenden-Button automatisch aktualisiert:

bewertungsformular2

bewertungsformular2

Man könnte an der Optik noch ein bisschen was verbessern, ich gebs ja zu, aber funktionieren tut die Sache einwandfrei. Wie gesagt, wenn man sich mit der WordPress-eigenen Ajax-Logik einmal auseinandergesetzt hat, ist so etwas nicht wirklich schwierig zu realisieren.

WordPress Stichwortsuche mit Treffer-Highlighting

Habt ihr euch auch schon manchmal gewünscht, WordPress würde die Suchergebnisse irgendwie kennzeichnen – zum Beispiel durch eine farbige Hinterlegung der gefundenen Begriffe? Manchmal ist es nämlich gar nicht so leicht herauszufinden, wo im Text sich der gesuchte Begriff (oder die gesuchten Begriffe) verstecken. Das eine durchaus übliche Sache, und ich hab mir mal ein paar Gedanken zu dem Thema gemacht und mich im Inselfisch-Kochbuch an die Umsetzung gemacht. Dabei ist ein Plugin herausgekommen, das sicher noch ein bisschen Optimierungspotential hat, aber so im Prinzip funktioniert die Sache. Wurde eh schon lang wieder einmal Zeit für ein bisschen Spaß auf der Datenbank!

Die Anforderungen

  • Es sollen ein oder mehrere Suchbegriffe (durch Leerzeichen getrennt) eingegeben werden können.
  • Zuerst sollen alle Rezepte ausgegeben werden, bei denen die gesuchten Begriffe schon im Titel vorkommen. Die Treffer werden farbig hinterlegt.
  • Dann sollen noch alle Rezepte nachfolgen, bei denen die Suchbegriffe im Rezepttext = post content vorkommen, auch hier farbig hinterlegt
  • Durch Klick auf den Rezepttitel soll man direkt zum Rezepttext gelangen.

Wir fangen natürlich mit einem Formular an

Das Plugin fängt mit einem einfachen Eingabefeld an:

/*
Plugin Name: Evis Suche mit Highlighting
Plugin URI: http://localhost/wp_ajax/wp-content/plugins/suche
Description: Eigenes Suchmodul mit Highlighting
Version: 1.0
Author: Evi Leu
Author URI: http://www.evileu.de
*/

//Formular als Shortcode einbinden
 function suche_form(){
     
    echo "<h3>Rezeptsuche mit Highlighting</h3>";
    echo "<p>Mehrere Stichworte mit Leerzeichen trennen</p>";
    
    
    echo "<form action='#' method='post'>";
    echo "<input type='text' size='100' name = 'suchstring' >";
    echo "</form>";
    

Der Inhalt des Textfeldes wird übernommen, wenn der Anwender auf Return drückt. Dann legen wir los:

if (isset($_POST['suchstring'])){
    
    //***ein oder mehrere Leerzeichen im String?
    if (strpos($_POST['suchstring']," ")>0){
        
        $suchsql = "";
        $suchsql_content = "";
        //string am leerzeichen trennen
        $strings = explode(" ",$_POST['suchstring']);
        $anzahl = count($strings);
        $ersterstring = $strings[0];
        for ($i=1; $i < $anzahl; $i++){
            //suchstring für post_title zusammenbauen
            $suchsql = $suchsql." AND post_title like '%".$strings[$i]."%' ";
            //suchstring für post_content zusammenbauen
            $suchsql_content = $suchsql_content." AND post_content like '%".$strings[$i]."%' ";
        }    
    }
    //***kein Leerzeichen im String
    else{$ersterstring = $_POST['suchstring'];
        
    }

Was hab ich gemacht? Wenn ein oder mehrere Leerzeichen im Suchstring vorhanden sind, zerlege ich den String mit explode() in ein Array. Dann baue ich mir aus allen vorkommenden Einzelstrings ein SQL-Snippet zusammen, eins für die Abfrage des post_title und eins für die Abfrage des post_content. Man kann die AND-Klauseln einfach hintereinanderhängen, es sollen ja alle Suchstrings gefunden werden. Groß/Kleinschreibung ist egal, der MySQL-Like ist nicht case sensitive. Wenn kein Leerzeichen im Suchbegriff vorkommt, übernehme ich ihn im Ganzen.

Die erste Abfrage geht auf den Titel

Mit dem zusammengebauten SQL-Snippet von oben starte ich die erste Abfrage: Dann gebe ich erstmal aus, wieviele Rezepte mit den Suchbegriffen im Titel gefunden wurden.

global $wpdb; 

    
    /******Suchstring im Titel?****/
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts 
where post_title like '%".$ersterstring."%' ".$suchsql."
    and post_type like 'post' and post_status like 'publish' order by post_title");
    
    $gefunden = $wpdb->num_rows;
    if ($gefunden == 1){
        
        echo $gefunden." Rezept mit ".$_POST['suchstring']." im Titel:";
    }
    if ($gefunden > 1){
        
        echo $gefunden." Rezepte mit ".$_POST['suchstring']." im Titel:";
    }

Dann klappere ich mit einem foreach alle Ergebnisse des ersten Selects ab und mache mit PHP ein paar Ersetzungen für das Highlighting der Suchbegriffe. Dafür benutze ich den str_ireplace, der ist nicht case sensitive. Dabei geht der erste Suchebegriff extra, für alle weiteren steppe ich mit der for-Anweisung durch unser Array von oben.

foreach($alleposts as $einpost){
        
        
        //**** ersten Suchstring im Titel highlighten
        $replace = '<span style="background-color: #FF0;">' . $ersterstring. '</span>'; // create replacement
        $content = str_ireplace( $ersterstring, $replace, $einpost->post_title );
        //***** ersten Suchstring im content highlighten
        $rezepttext = str_ireplace( $ersterstring, $replace, $einpost->post_content );
        
        //****alle weiteren Suchstrings highlighten
        for ($j=1; $j<$anzahl;$j++){
            
            $replace = '<span style="background-color: #FF0;">' . $strings[$j]. '</span>'; // create replacement
            $content = str_ireplace( $strings[$j], $replace, $content );
            $rezepttext = str_ireplace( $strings[$j], $replace, $rezepttext );
        }
        //als Accordeon ausgeben
        echo "<details><summary>".$content."</summary>";
        echo $rezepttext."</details>";
        echo "<hr>";
            
    }

Ganz am Ende benutze ich eine HTML5-Syntax für die Ausgabe des Rezepttitels als anklickbares Detail/Summary Element, auch Accordeon genannt. Das sieht dann so aus:

accordeon

accordeon

Wenn man auf das Dreieckssymbol klickt, wird der Rezepttext eingeblendet — mit Highlighting:

schweinefilet_highlighted

schweinefilet_highlighted

Das funktioniert auch mit mehreren Stichworten (es können beliebig viele sein), ich hab hier mal „butter tomate“ eingegeben:

butter_tomate

butter_tomate

Und jetzt das ganz nochmal auf den Content:

Der SQL ist nur minimal verändert und geht jetzt auf den post_content, das Highlighting funktioniert genauso wie oben, auch die Ausgabe als Accordeon:

//*******Suchstring im Content */
    
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts where 
         post_content like '%".$ersterstring."%' ".$suchsql_content." 
    and post_type like 'post' and post_status like 'publish' order by post_title");
    
    foreach($alleposts as $einpost){
        
        //**** ersten Suchstring highlighten
        $replace = '<span style="background-color: #FF0;">' . $ersterstring. '</span>'; // create replacement
        $rezepttext = str_ireplace( $ersterstring, $replace, $einpost->post_content );
        
        //****alle weiteren Suchstrings highlighten
        for ($j=1; $j<$anzahl;$j++){
            
            $replace = '<span style="background-color: #FF0;">' . $strings[$j]. '</span>'; // create replacement
            $rezepttext = str_ireplace( $strings[$j], $replace, $rezepttext );
        }
        //als Accordeon ausgeben
        echo "<details><summary>".$einpost->post_title."</summary>";
        echo $rezepttext."</details>";
        echo "<hr>";
        //}
        
    }

Das wars schon! Jetzt könnte man höchstens noch anmeckern, dass manche Rezepte zweimal auftauchen, weil sie einmal mit der Suche im Titel und ein zweites Mal mit der Suche im Content gefunden wurden, aber auch dafür sollte es eine Lösung  geben, das ist Feintuning.

Viel Spaß beim Nachbauen! Hier noch ein Beispiel, dass es auch mit mehreren Stichworten funktioniert, ich habe „butter tomate olivenöl hackfleisch knoblauch“ eingegeben:

5_suchworte

5_suchworte

Bemerkung nach Testbetrieb: immer diese Umlauts

Darauf bin ich erst später gekommen: der MySQL-Like ignoriert in der Kollation utf8_general_ci die deutschen Umlaute. So wird zum Beispiel zum Stichwort „huhn“ auch „hühnchen“ gefunden, da ü wird als u behandelt. Das wäre ja soweit noch nicht weiter tragisch, aber der PHP str_ireplace() macht sehr wohl einen Unterschied zwischen Umlauten und aeoui, da fällt das Highlighting auf die Nase… na ja, ich finde das jetzt nicht wirklich schlimm, es ist halt ein kleines Manko. Deswegen jetzt mit der Datenbankkollation rumzuexperimentieren erspare ich mir, das wäre mit Kanonen auf Spatzen geschossen.

Doppelt vorkommende Rezepte ausblenden

Bislang kamen Rezepte, die sowohl im Titel als auch im Content die eingegebenen Begriffe enthielten, doppelt vor. Das läßt sich relativ einfach unterbinden, ich merke mir einfach in einem Array, welche Rezept-IDs bei der Stichwortsuche im Titel schon gefunden wurden, und baue daraus einen String für eine not-in-Klausel. Dafür kommt noch folgender Code nach dem ersten SQL rein:

//merken, welche post ids schon gefunden wurden
    $merken = array();
    
    //Falls ein oder mehrere Rezepte gefunden wurden, IDs merken
    if ($gefunden>0){
        
            $k=0;
            foreach ($alleposts as $einpost){
            $merken[$k] = $einpost->ID;
            $k=$k+1;
            }
        }else{
            //Falls kein Rezept gefunden wurde, Dummy Eintrag mit ID 0
            $gemerkt = "(0)";
            }
    
    //Klammer für den not in aufbauen
    $gemerkt = "(";
    for ($l=0; $l < $gefunden; $l++){
   
    $gemerkt = $gemerkt.$merken[$l].", ";
    
    }
    //Klammer für den "not in" mit Dummy-Eintrag 0 abschliessen
    $gemerkt = $gemerkt."0)";

Den Dummy-Eintrag mit der 0 brauchts, damit der „not in“ nicht auf eine leere Klammer rennt und gar nichts ausgibt. Mit dem jetzt in der Variable $gemerkt gespeicherten String mit den IDs gehe ich in den zweiten SQL, der im Content sucht, und füge noch eine „not in“-Klausel  hinzu:

$alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts where 
         post_content like '%".$ersterstring."%' ".$suchsql_content." 
    and post_type like 'post' and post_status like 'publish' 
and ID not in ".$gemerkt." order by post_title");

Das filtert alle Rezepte heraus, die bereits in der ersten SQL-Abfrage gefunden wurden.

 

HTML5 Datalist Value – ganz so einfach ist es nicht

Eigentlich gibt es in HTML ja die Option, einem Dropdownfeld zusätzlich zur Bezeichnung einen Value mitzugeben, der dann bei der Auswahl weitergegeben wird. In meinem Fall wäre die Bezeichnung der Titel des Rezeptes, und der Value die ID aus der Datenbank. Das würde dann etwa so aussehen:

...
<option value = '345'>Dampfnudeln wie bei Oma</option>"
...

Sobald der Benutzer den Eintrag Dampfnudeln ausgewählt und auf Return gedrückt hat, wird der Wert 345 übergeben – sollte zumindest. Diese Option funktioniert aber bei einer Datalist browserabhängig ganz unterschiedlich, wäre ja zu einfach.

In Chrome

…kriege ich die numerischen IDs in der Auswahlliste mit angezeigt, das ist nicht besonders schön, funktioniert aber wenigstens.

chrome_value

chrome_value

Es wird auch richtig bei der Auswahl die ID weitergegeben.

In Firefox Quantum

wird der Value-Wert einfach ignoriert, ich bekomme nur die Titel angezeigt und bei der Auswahl ausgegeben.

firefox_value

firefox_value

Im Internet Explorer 10

wirds besonders g’schmackig, da kann man gar nicht mehr nach dem Titel auswählen, der Autocomplete funktioniert erst wieder, wenn ich den Option Value ganz herausnehme.

Was nun? Ich mach mal kurzen Prozeß

Bevor ich mir hier für die unterschiedlichen Varianten der einzelnen Browserausgaben einen Wolf programmiere, verzichte ich ganz auf die Übergabe der ID und suche meine Rezepte schlicht nach dem Titel aus der Datenbank. Das kann ich mir erlauben, weil a) ich nur ein-eindeutige Titel habe und b) die Schreibweise aus dem Dropdownfeld identisch ist mit der Schreibweise in der wp_posts. Falls der User dann noch zusätzliche Zeichen eingibt (woran ihn nieman hindert) wird halt kein Rezept gefunden, aber damit kann ich leben. Um c) auszuschliessen, dass Revisions oder attachments mit reinrutschen, erweitere ich meinen Select einfach um post_type like ‚post‘ and post_status like ‚publish‘, aber das reicht dann.

Das Ganze klemme ich in eine if(isset($_POST)…Abfrage und packe es in den Shortcode für das Formular mit rein, das sieht dann so aus:

//*********Formular als Shortcode einbinden
 function asuche(){ 
    echo "<form action='#' method='post'>";
    echo "<input type = 'text' name = 'ausgesucht' size = '100' id = 'textfeld' autocomplete = 'off' onkeyup = 'myFunction(this.value);' list = 'suchliste'>";
    
    echo "<datalist id='suchliste'>";
    echo "<div id = 'liste'>";
    echo "</div>";    
    echo "</datalist>";
    echo "</form>";
     if (isset($_POST['ausgesucht'])){
             
    /*********Action*/     
    global $wpdb;
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts 
where post_title like '".$_POST['ausgesucht']."'
 and post_status like 'publish' and post_type like 'post'");
    foreach($alleposts as $einpost){
        
        echo "<h1>".$einpost->post_title."</h1>";
        echo $einpost->post_content;
    }
    /*********End Action*/
    }    
    
 }
 add_shortcode('asuche', 'asuche');

Damit wird folgende Funktionalität realisiert: der User kann aus der Autocomplete-Liste einen Eintrag auswählen und mit Return in das Textfeld übernehmen. Bei nochmaligem Drücken von Return wird der komplette Text des ausgewählten Rezeptes angezeigt. Das ist mir schön genug, und eingängig in der Bedienung.

Nur der Internet-Explorer macht noch Zicken, der schlägt beharrlich nur Rezepte vor, die mit dem eingegebenen Suchbegriff anfangen, nicht alle in denen der Begriff irgendwo im Titeltext vorkommt. Sei es drum, in Chrome und Firefox funktioniert es jedenfalls wie beabsichtigt.

WordPress AJAX Autocomplete für Minimalisten

Ich bin ja bekanntermassen ziemlich hartnäckig, und obwohl ich die Lösung für das Autocomplete-Feld von David Nash toll finde, hats mir doch keine Ruhe gelassen. Das muss doch noch ein bisschen einfacher gehen, dachte ich mir und hab den Codex durchforstet. Zum Thema Using Ajax in Plugins bin ich fündig geworden, da stand doch ein (Codex-unüblich nachvollziehbares) Beispiel drin, über das habe ich mich hergemacht. Das Javascript für den Ajax-Call wird hier nicht als externes Script enqueued, sondern in die Plugin-Datei integriert. Ist aber nicht schlimm, das Script ist nämlich sehr überschaubar – aber jetzt mal der Reihe nach.

Die Vorgabe

Ich möchte gern ein Textfeld, in das ich Suchbegriffe eingeben kann, und das soll mir dann aus der Datenbank alle Post Titles ausgeben, in denen der Suchbegriff vorkommt. Das soll etwa so aussehen:

schoko

schoko

Wir basteln uns ein Plugin

Natürlich brauchen wir dazu wieder ein Formular, und ich werde auch wieder die HTML5-Datalist verwenden. Wir packen das Ganze in ein Plugin, damit wir vom Theme unabhängig sind, und tun Formular und Javascript in einen Shortcode, damit wir das ganze in einem beliebigen Beitrag oder auf einer beliebigen Seite auch ausgeben können.

Zuerst mal nur das Plugin ohne JScript und Ajax-Funktion:

<?php
/*
Plugin Name: WordPressAjax Testplugin
Plugin URI: http://localhost/wp_ajax/wp-content/plugins/wp-ajax
Description: Zum Testen von Ajax mit WordPress
Version: 1.0
Author: Evi Leu
Author URI: http://www.evileu.de
*/


/*************************************************************/
//*********Formular als Shortcode einbinden
 function asuche(){
    
    echo "<input type = 'text' id = 'textfeld' autocomplete = 'off'
 onkeyup = 'myFunction(this.value);' list = 'suchliste'>";
    
    echo "<datalist id='suchliste'>";
    echo "<div id = 'liste'>";
    echo "</div>";    
    echo "</datalist>";
    
 }
 add_shortcode('asuche', 'asuche');

Ich hab hier schon mal den Funktionsaufruf bei onkeyup mit drin, der kriegt als Parameter den Inhalt des Textfeldes mit, Script kommt gleich. Die Verbindung vom Textfeld zur Datalist geht über die id suchliste. Autocomplete = ‚off‘ damit der Browser nicht reinpfuscht. In der Datalist hab ich eine weitere Div mit der id liste angelegt, in die schreibt nachher mein Ajax-Call seine Rückgabewerte.

Das integrierte Javascript

Wie gesagt, das Scripterl ist so kurz, dass man es nicht in eine externe Datei auslagern muss, wir nehmen es in die Funktion asuche mit rein, das sieht dann so aus:

function asuche(){ ?>
     
    <script>
        function myFunction(wert){
              
        var aktwert = wert;
        var data = {
            'action': 'my_action',
            'whatever': aktwert
            
        };

        // since 2.8 ajaxurl is always defined in the admin header and points to admin-ajax.php
        // wir sind aber nicht auf den Admin-Pages, deswegen der volle Pfad
        jQuery.post('http://localhost/wp_ajax/wp-admin/admin-ajax.php', data, function(response) {
        //    jQuery.post(ajaxurl, data, function(response) {
            
            document.getElementById("liste").innerHTML = response;
            
        });
        } //*********************End myFunction
        
    </script>
    <?php
...

Was passiert hier? Zuerstmal wird der als Parameter übergebene Wert des Textfeldes auf die Variable aktwert gelegt, diese kommt in die data-Parameter des Ajaxcalls mit rein. Der erste Parameter mit dem ‚action‘ definiert, welche Funktion nachher aufgerufen werden soll, die kommt gleich im Anschluss. Mit dem JQuery.post(…) wird die Anfrage an den admin-ajax.php weitergeleitet und die response definiert. Die macht nichts anderes als die Div mit der id liste mit dem Rückgabewert der aufgerufenen Funktion zu belegen, konkret füllt sie die Options-Liste unserer Datalist.

Was weniger schön ist: die Url zur admin-ajax.php steht hier noch als langer Rattenschwanz mit drin, das könnte man sicher schöner lösen. Wenn man zum Beispiel das JQuery-Script auslagern und per enqueue laden würde, könnte man mit wp_localize_script den Pfad als Variable mitgeben – aber das sind jetzt schon Feinheiten.

Damit der Funktionsaufruf klappt: Add Action

Ich habs zweimal drin, sowohl für die Admin-Ansicht als auch für nicht eingeloggte User:

add_action( 'wp_ajax_my_action', 'my_action' );
add_action( 'wp_ajax_nopriv_my_action', 'my_action' );

Jetzt fehlt uns noch die tatsächliche Funktion, die den Übergebenen Parameter aktwert entgegennimmt und und die passenden  Werte aus der Datenbank abruft.

Die PHP-Funktion

Da wir uns im Kontext von WordPress bewegen, brauchen wir keinen externen Datenbankzugriff mit mysqli oder PDO, sondern können wie gewohnt mit dem $wpdb-Objekt arbeiten. Die Funktion sieht so aus:

function my_action() {
    global $wpdb; 

    $whatever = $_POST['whatever'];
    
    /**********************************************/
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts 
where post_title like '%".$whatever."%' 
    and post_type like 'post' and post_status like 'publish'");
    foreach($alleposts as $einpost){
        
        echo "<option>".$einpost->post_title."</option>";
}
    /**********************************************/
    
    
    wp_die(); // this is required to terminate immediately and return a proper response
} //**********End function my_action

Unsere übergebene Variable whatever wird nicht anders verarbeitet, als ob sie aus einem Formular direkt käme, dafür sorgt der Call mit dem jQuery.post(…). Der Rest ist wirklich simpel, ich suche mir aus der Tabelle wp_posts alle Einträge, die unseren Suchbegriff matchen, gebe die gefundenen Post Titel mit der Foreach-Schleife aus und klebe noch die Option-Tags für unsere Datalist dran. Fertig ist unser Autocomplete-Feld!

suchwort_back

suchwort_back

Nachtrag: JS ausgelagert und Pfad übergeben

Wie ich oben schon angemerkt habe, ist es nicht so ideal dass im Javascript der volle Pfad zur admin-ajax.php steht. Das geht auch besser. Das Script kommt aus der Plugin-Datei raus, ich nenne es mal wp_ajax.js, und es sieht so aus:

function myFunction(wert){
                
        var aktwert = wert;
        var data = {
            'action': 'my_action',
            'whatever': aktwert
            };

        // der volle Pfad zur admin-ajax.php liegt auf my_ajaxurl
        jQuery.post(my_ajaxurl, data, function(response) {
                    
            document.getElementById("liste").innerHTML = response;
            
        });
        } //*********************End myFunction

Die Variable my_ajaxurl kriegt ihren Wert über wp_localize_script, das geht in einem Aufwasch mit dem Einbinden der externen Javascript-Datei, dazu stellt man folgenden Code an den Anfang des Plugins:

/*********Externes Script einbinden und Pfad zur admin-ajax.php übergeben*/
function mysite_js() {
      
    wp_enqueue_script('mysite-js', plugins_url().'/wp_ajax/wp_ajax.js', array('jquery'), false, false);
    
    wp_localize_script( 'mysite-js', 'my_ajaxurl', admin_url( 'admin-ajax.php' ) );
 
   }
add_action('wp_enqueue_scripts', 'mysite_js');

Dann kann das JS aus der Plugindatei raus, das sorgt für mehr Übersicht und ist wesentlich eleganter gelöst.

Was ich jetzt noch gern hätte

Wenn man einen Rezepttitel angewählt und auf Return gedrückt hat, möchte ich gerne das gewählte Rezept anzeigen lassen. Aber dazu muss ich mir noch ein, zwei Gedanken machen, meine bisherige Lösung mit der angehängten Rezept-ID ist nämlich eine ziemliche Krücke, das müsste eleganter gehen. Ich geh mal forschen…. und dafür gibts dann einen neuen Beitrag.