JSFiddle - React, Tailwind, and code Playground

by Zemke

HTML

<!DOCTYPE html>
<html>
<head>
<script src="js.js"></script>
<script>
  window.onload = function () {
    const tpl = document.getElementById("selects");
    document.querySelectorAll('zemke-tagbox select')
      .forEach(elem => elem.innerHTML = tpl.innerHTML);
  };
</script>
</head>
<body>

<h1>Default</h1>

<form name="ex-from" method="POST" action="/">
  <zemke-tagbox>
    <input type="text" name="body" required>
    <select name="tags" multiple>
    </select>
  </zemke-tagbox>
  <input type="submit" value="Submit">
</form>

<h2>Styled</h2>

<div class="styled">
  <form name="styled-from" method="POST" action="/">
    <zemke-tagbox ci search="levenshtein,60" nothing="Keep typing...">
      <input type="text" name="body" required>
      <select name="tags" multiple>
      </select>
    </zemke-tagbox>
    <input type="submit" value="Submit">
  </form>
</div>

</body>
</html>

<template id="selects">
  <option value="1">James</option>
  <option value="2">Robert</option>
  <option value="3">John</option>
  <option value="4">Michael</option>
  <option value="5">David</option>
  <option value="6">William</option>
  <option value="7">Richard</option>
  <option value="8">Joseph</option>
  <option value="9">Thomas</option>
  <option value="10">Charles</option>
  <option value="11">Christopher</option>
  <option value="12">Daniel</option>
  <option value="13">Matthew</option>
  <option value="14">Anthony</option>
  <option value="15">Mark</option>
  <option value="16">Donald</option>
  <option value="17">Steven</option>
  <option value="18">Paul</option>
  <option value="19">Andrew</option>
  <option value="20">Joshua</option>
  <option value="21">Kenneth</option>
  <option value="22">Kevin</option>
  <option value="23">Brian</option>
  <option value="24">George</option>
  <option value="25">Timothy</option>
  <option value="26">Ronald</option>
  <option value="27">Edward</option>
  <option value="28">Jason</option>
  <option value="29">Jeffrey</option>
  <option...

CSS

.styled {
    margin-left: 10rem;
  }
  .styled zemke-tagbox::part(tag) {
    background-color: rgb(0, 255, 0, .3);
  }
  .styled zemke-tagbox::part(input) {
    background-color: yellow;
    font-family: monospace;
    font-size: 2rem;
    border: 3px dotted blue;
    padding-left: 40px;
    padding-right: 10px;
    border-left-width: 10px;
    border-right-width: 10px;
  }
  .styled zemke-tagbox::part(dropdown-container) {
    padding: 2rem;
  }
  .styled zemke-tagbox::part(dropdown-item) {
    letter-spacing: 1rem;
    color: limegreen;
  }

  input[type=submit] {
    margin-top: .3rem;
  }

  html, body {
   font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Oxygen, Cantarell, sans-serif; 
  }

JavaScript

class TagBox extends HTMLElement {

  static Matcher = class {

    static FALLBACK = 'start';
    static SEARCHES = [this.FALLBACK, 'infix', 'levenshtein'];

    constructor(q, ci, fn) {
      this.q = ci ? q.toLowerCase() : q;
      this.ci = ci;

      if (TagBox.Matcher.SEARCHES.indexOf(fn[0]) === -1) {
        console.warn(
          `Invalid search "${fn[0]}". Valid searches are ${TagBox.Matcher.SEARCHES.join(', ')}. Falling back to "${TagBox.Matcher.FALLBACK}".`);
        fn = [TagBox.Matcher.FALLBACK, ...fn.slice(1)];
      }
      this.fn = fn;
    }

    static parseSearchAttr(attr) {
      return !attr ? [TagBox.Matcher.FALLBACK] : attr.split(',').map(s => s.trim());
    }

    perform(s) {
      return this['_' + this.fn[0]](s, ...this.fn.slice(1));
    }

    _start(s) {
      return this._lc(s).startsWith(this.q);
    }

    _infix(s) {
      return this._lc(s).indexOf(this.q) !== -1;
    }

    _levenshtein(s, b=80) {
      if (this.q === '') {
        return true;
      }
      s = this._lc(s);
      const [sh, ln] = [s, this.q]
        .sort((a, b) => a.length - b.length)
      if (ln.length === 0) {
        return 1.0;
      }
      const costs = new Array();
      for (let i = 0; i <= ln.length; i++) {
        let lastValue = i;
        for (let j = 0; j <= sh.length; j++) {
          if (i == 0) {
            costs[j] = j;
            continue;
          }
          if (j > 0) {
            let newValue = costs[j - 1];
            if (ln.charAt(i - 1) != sh.charAt(j - 1))
              newValue = Math.min(Math.min(newValue, lastValue),
                costs[j]) + 1;
            costs[j - 1] = lastValue;
            lastValue = newValue;
          }
        }
        if (i > 0) {
          costs[sh.length] = lastValue;
        }
      }
      const v = (ln.length - costs[sh.length]) / parseFloat(ln.length);
      return v >= parseFloat(b / 100);
    }

    _lc(s) {
      return this.ci ? s.toLowerCase() : s;
    }
  };
  static DELIMITER =...