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 =...