knuspermagier.de
Since 2005.

Fancy Suchleiste

Stellt euch vor, ihr habt eine Rezepte-App mit einer Suche und ihr wolltet schon immer so eine Suchleiste, die ein bisschen so ist, wie in jeder halbwegs fancy Mail-App, oder so, und zwar, dass man neben der normalen Volltext-Suche auch so Facetten hat, nach denen man suchen kann. In den Mail-Apps sind das dann Sachen wie Absender und Empfänger, und hier halt zum Beispiel Zutaten oder Tags.

Nun ist die Frage, wie baut man sowas. Im Grunde ist es ein <input>-Feld, aber mit etwas fancyness, damit die Facetten automatisch erkannt und anders eingefärbt werden. Dazu soll man sie natürlich noch einfach mit einem Klick entfernen können, oder wenn man Backspace drückt, etc.

Aussehen soll es etwa so:

Zutat: Karotte Zutat: Dill lecker essen

Da ich gerne verstehe, wie etwas funktioniert, wollte ich es gerne selber bauen. Also abgesehen davon habe ich auch ca. vier Sekunden gegoogelt, aber fand auch kein npm-Package, dass das leistet. Suchte aber auch nur nach “fancy schmancy search box”.

Mein erster Gedanke war ein <input>-Feld zu nehmen, wo man Sachen eingibt, und das von einem <div> mit den gerenderten Elementen zu überdecken. Problem dabei: Wo bleibt der Cursor, und woher weiß man wo man gerade tippt? blöde Idee.

Da ich einen Blogpost daraus machen wollte, habe ich den Zwischenstand allerdings aufgehoben, hier eine Live Demo:

Nachdem ich mit der ersten sehr naiven Version nicht weiter kam, dachte ich kurz an contenteditable. Mit diesem Attribut kann man ja jedes HTML-Element zu einem Eingabefeld machen, toll! Ich las zwar in den letzten Jahren immer sehr viel Negatives, und es ist in meinem Kopf eher unter considered evil abgespeichert. Aber was solls!

Iframes sagen mehr als tausend Worte, hier also schonmal das Ergebnis:

Doch wie funktioniert es? Irgendwie low-leveliger, als mir lieb ist. Ich überwache die input-Events von meinem editierbaren <div> und entscheide für jede gedrückte Taste, was passieren soll. Letztendlich sammle ich immer alle Buchstaben in einem Buffer, und sobald ein Leerzeichen getippt wird, wird der aktuelle Buffer in ein Token-Array gespeichert und geleert. Bei nem Backspace wird der letzte Token gelöscht, etc etc.

    input(ev) {
        const type = ev.inputType;
        const char = ev.data;

        if(type === 'deleteContentBackward') {
            if(this.currentBuffer.trim().length) {
                this.currentBuffer = this.currentBuffer.substring(0, this.currentBuffer.length -1);
            } else {
                if(this.tokens.length) {
                    this.tokens.splice(this.tokens.length - 1, 1);
                }
            }
        } else {
            if(char === ' ') {
                this.tokens.push(this.currentBuffer);
                this.currentBuffer = ' ';
            }

            this.currentBuffer += char;
        }

        this.$refs.wurst.innerHTML = 
            this.render(this.tokens) + ' ' + this.currentBuffer;

        placeCaretAtEnd(this.$refs.wurst);
    },

Immer wenn ich solchen Code schreibe, der irgendwas mit einzelnen Zeichen macht, fühle ich mich schmutzig und denke, dass ich lieber eine fertige Library nehmen sollte, aber Ziel dieser Aktion war es ja, zu verstehen, wie das funktionieren könnte. Daher waschen wir uns kurz die Hände und machen weiter!

Nach jedem Event rendere ich dann alle Tokens neu in das <div> und setze den Cursor mit einer von Stack Overflow geklauten Funktion wieder ans Ende (hab kein Interesse daran, zu verstehen, wie man den Focus ans Ende setzt)

Tatsächlich funktioniert das schon ganz gut so und nach 20 Minuten rumprobieren war meine Neugier auch erstmal gestillt.

Der nächste Schritt wäre jetzt, die einzelnen Tokens als Vue-Komponenten zu rendern, damit ich ihnen mehr Funktionen geben kann. Ob das klappt, erzähle ich beim nächsten Mal!

Kommentare, Feedback und andere Anmerkungen?
Schreib mir eine E-Mail 🤓