Skip to content

Instantly share code, notes, and snippets.

@cagataycali
Last active November 25, 2021 02:36
Show Gist options
  • Save cagataycali/78b4b4972bb4d03096d82aaeb4a1591a to your computer and use it in GitHub Desktop.
Save cagataycali/78b4b4972bb4d03096d82aaeb4a1591a to your computer and use it in GitHub Desktop.
[HTML + CSS + JS] Simple auto complete
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Auto Complete</title>
<style>
#container {
width: 200px;
}
.input {
width: 200px;
}
.list {
padding-left: 0px;
margin-top: 0.2rem;
}
.item {
list-style: none;
}
.item:hover {
border-bottom: 1px dotted #333;
}
.item.active {
border-bottom: 1px dotted #333;
}
.empty {
display: none;
}
</style>
</head>
<body>
<div id="container">
<input
type="text"
class="input"
value=""
placeholder="Search fruit"
aria-label="Search fruit"
/>
<ul class="list"></ul>
</div>
<script>
const fruits = [
"apple",
"apricot",
"avocado",
"banana",
"bell pepper",
"bilberry",
"blackberry",
"blackcurrant",
"blood orange",
"blueberry",
"boysenberry",
"breadfruit",
"canary melon",
"cantaloupe",
"cherimoya",
"cherry",
"chili pepper",
"clementine",
"cloudberry",
"coconut",
"cranberry",
"cucumber",
"currant",
"damson",
"date",
"dragonfruit",
"durian",
"eggplant",
"elderberry",
"feijoa",
"fig",
"goji berry",
"gooseberry",
"grape",
"grapefruit",
"guava",
"honeydew",
"huckleberry",
"jackfruit",
"jambul",
"jujube",
"kiwi fruit",
"kumquat",
"lemon",
"lime",
"loquat",
"lychee",
"mandarine",
"mango",
"mulberry",
"nectarine",
"nut",
"olive",
"orange",
"papaya",
"passionfruit",
"peach",
"pear",
"persimmon",
"physalis",
"pineapple",
"plum",
"pomegranate",
"pomelo",
"purple mangosteen",
"quince",
"raisin",
"rambutan",
"raspberry",
"redcurrant",
"rock melon",
"salal berry",
"satsuma",
"star fruit",
"strawberry",
"tamarillo",
"tangerine",
"tomato",
"ugli fruit",
"watermelon",
];
function debounce(func, wait) {
let timer = null;
return function () {
const later = () => {
timer = null;
func.apply(this, arguments);
};
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(later, wait);
};
}
// Implement Trie (Prefix tree)
class Node {
constructor(char) {
this.char = char;
this.isEndWord = false;
this.children = new Map(); // Hashmap,
}
}
class Trie {
constructor() {
this.root = new Node("");
}
insert = (...words) => {
for (const word of words) {
let node = this.root;
for (const char of word) {
if (!node.children.has(char))
node.children.set(char, new Node(char));
node = node.children.get(char);
}
node.isEndWord = true;
}
};
autocomplete = (word) => {
const suggestions = [];
let node = this.root;
for (const char of word) {
if (!node.children.has(char)) return suggestions;
node = node.children.get(char);
}
this.helper(node, suggestions, word.substring(0, word.length - 1));
return suggestions;
};
helper = (node, suggestions, prefix) => {
if (node.isEndWord) suggestions.push(prefix + node.char);
for (const key of node.children.keys()) {
const childNode = node.children.get(key);
this.helper(childNode, suggestions, prefix + node.char);
}
};
}
class Autocomplete {
constructor(input, list, search, wait = 2000) {
this.keys = {
ArrowUp: "ArrowUp",
ArrowDown: "ArrowDown",
Tab: "Tab",
Enter: "Enter",
Escape: "Escape",
};
this.document = document;
this.input = input;
this.list = list;
this.search = search;
this.wait = wait;
this.selectedItemIndex = -1; // We'll update this after, for keyboard navigation.
this.input.addEventListener("keyup", this.handleInput);
}
handleInput = (e) => {
let previousItem;
let nextItem;
switch (e.key) {
case this.keys.ArrowDown:
previousItem = list.children[this.selectedItemIndex];
if (previousItem) {
previousItem.classList.remove("active");
}
this.selectedItemIndex++;
if (this.selectedItemIndex === list.children.length) {
this.selectedItemIndex = 0;
}
nextItem = list.children[this.selectedItemIndex];
if (nextItem) {
nextItem.classList.add("active");
}
break;
case this.keys.ArrowUp:
previousItem = list.children[this.selectedItemIndex];
if (previousItem) {
previousItem.classList.remove("active");
}
this.selectedItemIndex--;
if (this.selectedItemIndex === -1) {
input.focus();
}
nextItem = list.children[this.selectedItemIndex];
if (nextItem) {
nextItem.classList.add("active");
}
break;
case this.keys.Tab:
console.log("tab");
break;
case this.keys.Enter:
if (list.children[this.selectedItemIndex]) {
input.value = list.children[this.selectedItemIndex].innerText;
this.closeList();
}
break;
case this.keys.Escape:
this.closeList();
break;
default:
this.doSearch(e);
}
};
doSearch = debounce((e) => {
if (e.target.value.length === 0) {
this.closeList();
return;
}
const newItems = this.search(e.target.value);
this.renderItems(newItems);
}, this.wait);
closeList = () => {
this.list.textContent = "";
};
renderItems = (items) => {
const elementsToRender = [];
for (const item of items) {
const element = document.createElement("li");
element.innerText = item;
element.classList.add("item");
elementsToRender.push(element);
}
list.replaceChildren(...elementsToRender);
};
}
const [input, list] = [
document.querySelector(".input"),
document.querySelector(".list"),
];
const prefixTree = new Trie();
prefixTree.insert(...fruits);
const search = (text) => {
return prefixTree.autocomplete(text);
};
const autocomplete = new Autocomplete(input, list, search);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment